//go:build integration

// Package integration contains E2E tests that require external services.
// Run with: go test -tags integration ./test/integration/
// The tests spin up a real Keycloak instance via testcontainers-go, obtain
// a JWT token, and verify that the proxy auth middleware accepts / rejects it.
package integration

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	tc "github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"

	"github.com/veylant/ia-gateway/internal/middleware"
	"github.com/veylant/ia-gateway/internal/provider"
	"github.com/veylant/ia-gateway/internal/proxy"
	"go.uber.org/zap"
)

const (
	keycloakImage   = "quay.io/keycloak/keycloak:24.0"
	testRealm       = "veylant-test"
	testClientID    = "test-client"
	testClientSecret = "test-secret"
	testUsername    = "testuser"
	testPassword    = "testpass"
)

// startKeycloak starts a Keycloak container and returns its base URL.
func startKeycloak(t *testing.T) (baseURL string, cleanup func()) {
	t.Helper()
	ctx := context.Background()

	req := tc.ContainerRequest{
		Image:        keycloakImage,
		ExposedPorts: []string{"8080/tcp"},
		Env: map[string]string{
			"KC_BOOTSTRAP_ADMIN_USERNAME": "admin",
			"KC_BOOTSTRAP_ADMIN_PASSWORD": "admin",
		},
		Cmd: []string{"start-dev"},
		WaitingFor: wait.ForHTTP("/health/ready").
			WithPort("8080/tcp").
			WithStartupTimeout(120 * time.Second),
	}

	container, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	require.NoError(t, err, "starting Keycloak container")

	host, err := container.Host(ctx)
	require.NoError(t, err)
	port, err := container.MappedPort(ctx, "8080")
	require.NoError(t, err)

	base := fmt.Sprintf("http://%s:%s", host, port.Port())

	// Provision realm + client + test user via Keycloak Admin REST API.
	provisionKeycloak(t, base)

	return base, func() {
		_ = container.Terminate(ctx)
	}
}

// provisionKeycloak creates the test realm, client, and user via the Admin API.
func provisionKeycloak(t *testing.T, baseURL string) {
	t.Helper()
	ctx := context.Background()

	// Obtain admin access token.
	adminToken := getAdminToken(t, baseURL)

	adminAPI := baseURL + "/admin/realms"
	client := &http.Client{Timeout: 10 * time.Second}

	doJSON := func(method, path string, body interface{}) {
		t.Helper()
		var r io.Reader
		if body != nil {
			b, _ := json.Marshal(body)
			r = strings.NewReader(string(b))
		}
		req, _ := http.NewRequestWithContext(ctx, method, adminAPI+path, r)
		req.Header.Set("Authorization", "Bearer "+adminToken)
		req.Header.Set("Content-Type", "application/json")
		resp, err := client.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close() //nolint:errcheck
		require.Less(t, resp.StatusCode, 300,
			"Keycloak admin API %s %s returned %d", method, path, resp.StatusCode)
	}

	// Create realm.
	doJSON(http.MethodPost, "", map[string]interface{}{
		"realm":   testRealm,
		"enabled": true,
	})

	// Create public client with direct access grants (password flow for testing).
	doJSON(http.MethodPost, "/"+testRealm+"/clients", map[string]interface{}{
		"clientId":                  testClientID,
		"enabled":                   true,
		"publicClient":              false,
		"secret":                    testClientSecret,
		"directAccessGrantsEnabled": true,
		"standardFlowEnabled":       false,
	})

	// Create test user.
	doJSON(http.MethodPost, "/"+testRealm+"/users", map[string]interface{}{
		"username":      testUsername,
		"email":         testUsername + "@example.com",
		"enabled":       true,
		"emailVerified": true,
		"credentials": []map[string]interface{}{
			{"type": "password", "value": testPassword, "temporary": false},
		},
	})
}

func getAdminToken(t *testing.T, baseURL string) string {
	t.Helper()
	resp, err := http.PostForm(
		baseURL+"/realms/master/protocol/openid-connect/token",
		url.Values{
			"grant_type": {"password"},
			"client_id":  {"admin-cli"},
			"username":   {"admin"},
			"password":   {"admin"},
		},
	)
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck
	var tok struct {
		AccessToken string `json:"access_token"`
	}
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&tok))
	return tok.AccessToken
}

func getUserToken(t *testing.T, baseURL string) string {
	t.Helper()
	resp, err := http.PostForm(
		baseURL+"/realms/"+testRealm+"/protocol/openid-connect/token",
		url.Values{
			"grant_type":    {"password"},
			"client_id":     {testClientID},
			"client_secret": {testClientSecret},
			"username":      {testUsername},
			"password":      {testPassword},
		},
	)
	require.NoError(t, err)
	defer resp.Body.Close() //nolint:errcheck
	var tok struct {
		AccessToken string `json:"access_token"`
	}
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&tok))
	require.NotEmpty(t, tok.AccessToken, "could not obtain user token")
	return tok.AccessToken
}

// TestAuth_Integration_ValidToken_Returns200 obtains a real JWT from a live
// Keycloak instance and verifies that the Auth middleware + proxy handler
// accept it and call the next handler.
func TestAuth_Integration_ValidToken_Returns200(t *testing.T) {
	keycloakBase, cleanup := startKeycloak(t)
	defer cleanup()

	issuerURL := keycloakBase + "/realms/" + testRealm
	verifier, err := middleware.NewOIDCVerifier(context.Background(), issuerURL, testClientID)
	require.NoError(t, err)

	// Build a minimal proxy handler backed by a stub adapter.
	stub := &stubIntegrationAdapter{}
	handler := proxy.New(stub, zap.NewNop(), nil)

	// Wire up the auth middleware.
	protected := middleware.Auth(verifier)(handler)

	// Obtain a real JWT.
	token := getUserToken(t, keycloakBase)

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions",
		strings.NewReader(`{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}`))
	req.Header.Set("Authorization", "Bearer "+token)
	rec := httptest.NewRecorder()

	protected.ServeHTTP(rec, req)
	assert.Equal(t, http.StatusOK, rec.Code)
}

// TestAuth_Integration_NoToken_Returns401 verifies that the Auth middleware
// rejects requests without a token with 401.
func TestAuth_Integration_NoToken_Returns401(t *testing.T) {
	// We do not need a running Keycloak for this test — the missing token is
	// caught before any JWKS call.
	verifier := &middleware.MockVerifier{} // won't be called

	handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions",
		strings.NewReader(`{}`))
	rec := httptest.NewRecorder()

	middleware.Auth(verifier)(handler).ServeHTTP(rec, req)
	assert.Equal(t, http.StatusUnauthorized, rec.Code)
}

// stubIntegrationAdapter is a minimal provider.Adapter for integration tests.
type stubIntegrationAdapter struct{}

func (s *stubIntegrationAdapter) Validate(req *provider.ChatRequest) error { return nil }

func (s *stubIntegrationAdapter) Send(_ context.Context, _ *provider.ChatRequest) (*provider.ChatResponse, error) {
	return &provider.ChatResponse{ID: "integ-test", Model: "gpt-4o"}, nil
}

func (s *stubIntegrationAdapter) Stream(_ context.Context, _ *provider.ChatRequest, w http.ResponseWriter) error {
	fmt.Fprintf(w, "data: [DONE]\n\n") //nolint:errcheck
	return nil
}

func (s *stubIntegrationAdapter) HealthCheck(_ context.Context) error { return nil }
