package main

import (
	"baron-sso-backend/internal/domain"
	authhandler "baron-sso-backend/internal/handler"
	"baron-sso-backend/internal/middleware"
	"baron-sso-backend/internal/service"
	"baron-sso-backend/internal/testsupport"
	"bytes"
	"context"
	"crypto/rand"
	"crypto/rsa"
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/go-jose/go-jose/v4"
	josejwt "github.com/go-jose/go-jose/v4/jwt"
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/recover"
	"github.com/gofiber/fiber/v2/middleware/requestid"
	"github.com/stretchr/testify/mock"
)

type roundTripFunc func(req *http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
	return f(req)
}

type e2eMockIdentityProvider struct {
	mock.Mock
}

func (m *e2eMockIdentityProvider) Name() string {
	return "mock-idp"
}

func (m *e2eMockIdentityProvider) GetMetadata() (*domain.IDPMetadata, error) {
	return nil, nil
}

func (m *e2eMockIdentityProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) {
	return "", nil
}

func (m *e2eMockIdentityProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) {
	args := m.Called(loginID, password)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*domain.AuthInfo), args.Error(1)
}

func (m *e2eMockIdentityProvider) UserExists(loginID string) (bool, error) {
	return true, nil
}

func (m *e2eMockIdentityProvider) IssueSession(loginID string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *e2eMockIdentityProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) {
	return nil, nil
}

func (m *e2eMockIdentityProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *e2eMockIdentityProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
	return nil, nil
}

func (m *e2eMockIdentityProvider) InitiatePasswordReset(loginID, redirectURL string) error {
	return nil
}

func (m *e2eMockIdentityProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {
	return nil, nil
}

func (m *e2eMockIdentityProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
	return nil
}

type e2eMockKratosAdminService struct {
	mock.Mock
}

func (m *e2eMockKratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
	args := m.Called(ctx, identifier)
	return args.String(0), args.Error(1)
}

func (m *e2eMockKratosAdminService) GetIdentity(ctx context.Context, id string) (*service.KratosIdentity, error) {
	args := m.Called(ctx, id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*service.KratosIdentity), args.Error(1)
}

func (m *e2eMockKratosAdminService) ListIdentities(ctx context.Context) ([]service.KratosIdentity, error) {
	return nil, nil
}

func (m *e2eMockKratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*service.KratosIdentity, error) {
	return nil, nil
}

func (m *e2eMockKratosAdminService) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
	return nil
}

func (m *e2eMockKratosAdminService) DeleteIdentity(ctx context.Context, identityID string) error {
	return nil
}

func (m *e2eMockKratosAdminService) ListIdentitySessions(ctx context.Context, identityID string) ([]service.KratosSession, error) {
	return nil, nil
}

func (m *e2eMockKratosAdminService) GetSession(ctx context.Context, sessionID string) (*service.KratosSession, error) {
	return nil, nil
}

func (m *e2eMockKratosAdminService) DeleteSession(ctx context.Context, sessionID string) error {
	return nil
}

func newHeadlessLoginE2EApp(h *authhandler.AuthHandler, appEnv string) *fiber.App {
	app := fiber.New(fiber.Config{
		DisableStartupMessage: true,
		ErrorHandler:          newErrorHandler(appEnv),
	})

	app.Use(requestid.New(requestid.Config{
		Generator: func() string {
			return "req-e2e-headless"
		},
	}))

	app.Use(func(c *fiber.Ctx) error {
		start := time.Now()
		err := c.Next()

		status := c.Response().StatusCode()
		if status < 400 {
			return err
		}

		msg := "http_request"
		if err != nil {
			msg = "http_request_error"
		}

		slog.Info(msg,
			"status", status,
			"method", c.Method(),
			"path", c.Path(),
			"latency", time.Since(start).String(),
			"ip", c.IP(),
			"req_id", c.GetRespHeader(fiber.HeaderXRequestID),
		)
		return err
	})

	app.Use(recover.New(recover.Config{EnableStackTrace: true}))
	app.Use(middleware.ErrorCodeEnricher())

	api := app.Group("/api/v1")
	auth := api.Group("/auth")
	auth.Post("/headless/password/login", h.HeadlessPasswordLogin)

	return app
}

func mustE2EHeadlessRSAJWK(t *testing.T) (*rsa.PrivateKey, map[string]any) {
	t.Helper()

	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("failed to generate rsa key: %v", err)
	}

	keySet := jose.JSONWebKeySet{
		Keys: []jose.JSONWebKey{
			{
				Key:       &privateKey.PublicKey,
				KeyID:     "test-kid",
				Use:       "sig",
				Algorithm: string(jose.RS256),
			},
		},
	}

	raw, err := json.Marshal(keySet)
	if err != nil {
		t.Fatalf("failed to marshal jwks: %v", err)
	}

	var jwks map[string]any
	if err := json.Unmarshal(raw, &jwks); err != nil {
		t.Fatalf("failed to decode jwks map: %v", err)
	}

	return privateKey, jwks
}

func mustE2EHeadlessClientAssertion(t *testing.T, privateKey *rsa.PrivateKey, clientID, audience string) string {
	t.Helper()

	signer, err := jose.NewSigner(jose.SigningKey{
		Algorithm: jose.RS256,
		Key: jose.JSONWebKey{
			Key:       privateKey,
			KeyID:     "test-kid",
			Use:       "sig",
			Algorithm: string(jose.RS256),
		},
	}, nil)
	if err != nil {
		t.Fatalf("failed to create signer: %v", err)
	}

	now := time.Now()
	raw, err := josejwt.Signed(signer).Claims(josejwt.Claims{
		Issuer:    clientID,
		Subject:   clientID,
		Audience:  josejwt.Audience{audience},
		Expiry:    josejwt.NewNumericDate(now.Add(5 * time.Minute)),
		IssuedAt:  josejwt.NewNumericDate(now),
		NotBefore: josejwt.NewNumericDate(now.Add(-1 * time.Minute)),
		ID:        "assertion-e2e",
	}).Serialize()
	if err != nil {
		t.Fatalf("failed to sign client assertion: %v", err)
	}

	return raw
}

func mockHydraTransportForE2E(handler http.Handler) http.RoundTripper {
	return roundTripFunc(func(req *http.Request) (*http.Response, error) {
		w := httptest.NewRecorder()
		handler.ServeHTTP(w, req)
		return w.Result(), nil
	})
}

func runHeadlessPasswordLoginE2E(
	t *testing.T,
	logger *slog.Logger,
	appEnv string,
	jwks map[string]any,
	clientAssertion string,
) (*http.Response, string) {
	return runHeadlessPasswordLoginE2ERequest(
		t,
		logger,
		appEnv,
		jwks,
		clientAssertion,
		"http://example.com/api/v1/auth/headless/password/login",
		nil,
	)
}

func runHeadlessPasswordLoginE2ERequest(
	t *testing.T,
	logger *slog.Logger,
	appEnv string,
	jwks map[string]any,
	clientAssertion string,
	requestURL string,
	headers map[string]string,
) (*http.Response, string) {
	t.Helper()
	if !testsupport.PortBindingAvailable() {
		t.Skip("skipping headless password login E2E tests because this environment cannot bind local TCP listeners")
	}

	logBuffer := &bytes.Buffer{}
	if logger == nil {
		logger = slog.New(slog.NewJSONHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelInfo}))
	}

	previous := slog.Default()
	slog.SetDefault(logger)
	t.Cleanup(func() {
		slog.SetDefault(previous)
	})

	mockIDP := new(e2eMockIdentityProvider)
	mockIDP.On("SignIn", "employee001", "password").Return(&domain.AuthInfo{
		SessionToken: &domain.Token{JWT: "valid-jwt"},
		Subject:      "kratos-identity-id",
	}, nil)

	mockKratos := new(e2eMockKratosAdminService)
	mockKratos.On("FindIdentityIDByIdentifier", mock.Anything, "employee001").Return("kratos-identity-id", nil)

	jwksBody, err := json.Marshal(jwks)
	if err != nil {
		t.Fatalf("failed to marshal jwks body: %v", err)
	}

	jwksServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write(jwksBody)
	}))
	t.Cleanup(jwksServer.Close)

	hydraHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login") && r.Method == http.MethodGet:
			_ = json.NewEncoder(w).Encode(domain.HydraLoginRequest{
				Challenge: "challenge-123",
				Client: domain.HydraClient{
					ClientID:                "headless-login-client",
					TokenEndpointAuthMethod: "none",
					Metadata: map[string]interface{}{
						"status":                              "active",
						"headless_login_enabled":              true,
						"headless_token_endpoint_auth_method": "private_key_jwt",
						"headless_jwks_uri":                   jwksServer.URL + "/.well-known/jwks.json",
					},
				},
			})
			return
		case strings.Contains(r.URL.Path, "/oauth2/auth/requests/login/accept") && r.Method == http.MethodPut:
			_ = json.NewEncoder(w).Encode(map[string]string{"redirect_to": "http://rp/cb"})
			return
		}

		http.NotFound(w, r)
	})

	h := &authhandler.AuthHandler{
		IdpProvider: mockIDP,
		KratosAdmin: mockKratos,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: mockHydraTransportForE2E(hydraHandler)},
		},
	}

	app := newHeadlessLoginE2EApp(h, appEnv)

	body, _ := json.Marshal(map[string]string{
		"client_id":        "headless-login-client",
		"client_assertion": clientAssertion,
		"loginId":          "employee001",
		"password":         "password",
		"login_challenge":  "challenge-123",
	})
	req := httptest.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}

	return resp, logBuffer.String()
}

func TestHeadlessPasswordLogin_E2E_ResponseIncludesDetailedCodeAndLogs(t *testing.T) {
	privateKey, jwks := mustE2EHeadlessRSAJWK(t)
	clientAssertion := mustE2EHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		"https://rp.example.com/oidc/token",
	)

	logBuffer := &bytes.Buffer{}
	logger := slog.New(slog.NewJSONHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelInfo}))

	resp, _ := runHeadlessPasswordLoginE2E(t, logger, "production", jwks, clientAssertion)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401, got %d, body=%s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response body: %v", err)
	}

	if got["code"] != "invalid_client_assertion_audience" {
		t.Fatalf("expected detailed code, got=%v", got["code"])
	}
	if got["error"] != "Client assertion audience mismatch" {
		t.Fatalf("expected detailed error message, got=%v", got["error"])
	}

	output := logBuffer.String()
	if !strings.Contains(output, "\"reason_code\":\"invalid_client_assertion_audience\"") {
		t.Fatalf("expected headless failure log to include detailed reason code, got=%s", output)
	}
	if !strings.Contains(output, "\"req_id\":\"req-e2e-headless\"") {
		t.Fatalf("expected logs to include request id, got=%s", output)
	}
	if !strings.Contains(output, "\"path\":\"/api/v1/auth/headless/password/login\"") {
		t.Fatalf("expected request path in logs, got=%s", output)
	}
}

func TestHeadlessPasswordLogin_E2E_DebugLogsIncludeDiagnostics(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	privateKey, jwks := mustE2EHeadlessRSAJWK(t)
	const receivedAudience = "https://sso.hmac.kr/api/v1/auth/headless/password/login"
	clientAssertion := mustE2EHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		receivedAudience,
	)

	logBuffer := &bytes.Buffer{}
	logger := slog.New(slog.NewJSONHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))

	resp, _ := runHeadlessPasswordLoginE2E(t, logger, "production", jwks, clientAssertion)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusUnauthorized {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 401, got %d, body=%s", resp.StatusCode, string(bodyBytes))
	}

	output := logBuffer.String()
	if !strings.Contains(output, "\"expected_audiences\"") {
		t.Fatalf("expected debug logs to include expected_audiences, got=%s", output)
	}
	if !strings.Contains(output, "\"received_audiences\"") {
		t.Fatalf("expected debug logs to include received_audiences, got=%s", output)
	}
	if !strings.Contains(output, "\"received_audiences_text\":\""+receivedAudience+"\"") {
		t.Fatalf("expected debug logs to include received_audiences_text with full URL, got=%s", output)
	}
	if !strings.Contains(output, "\"expected_audiences_text\":\"http://example.com/api/v1/auth/headless/password/login, /api/v1/auth/headless/password/login\"") {
		t.Fatalf("expected debug logs to include expected_audiences_text, got=%s", output)
	}
	if !strings.Contains(output, "\"login_challenge_prefix\":\"challenge-12\"") {
		t.Fatalf("expected debug logs to include login challenge prefix, got=%s", output)
	}
}

func TestHeadlessPasswordLogin_E2E_AcceptsForwardedHTTPSAudience(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "")

	privateKey, jwks := mustE2EHeadlessRSAJWK(t)
	const receivedAudience = "https://sso.hmac.kr/api/v1/auth/headless/password/login"
	clientAssertion := mustE2EHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		receivedAudience,
	)

	logBuffer := &bytes.Buffer{}
	logger := slog.New(slog.NewJSONHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))

	resp, output := runHeadlessPasswordLoginE2ERequest(
		t,
		logger,
		"production",
		jwks,
		clientAssertion,
		"http://sso.hmac.kr/api/v1/auth/headless/password/login",
		map[string]string{
			"X-Forwarded-Proto": "https",
			"X-Forwarded-Host":  "sso.hmac.kr",
		},
	)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200 for forwarded https audience, got %d, body=%s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response body: %v", err)
	}
	if got["redirectTo"] != "http://rp/cb" {
		t.Fatalf("expected redirectTo, got=%v", got["redirectTo"])
	}

	if strings.Contains(output, "\"reason_code\":\"invalid_client_assertion_audience\"") {
		t.Fatalf("did not expect audience mismatch log, got=%s", output)
	}
}

func TestHeadlessPasswordLogin_E2E_AcceptsConfiguredPublicHTTPSAudience(t *testing.T) {
	t.Setenv("BACKEND_PUBLIC_URL", "https://sso.hmac.kr")

	privateKey, jwks := mustE2EHeadlessRSAJWK(t)
	const receivedAudience = "https://sso.hmac.kr/api/v1/auth/headless/password/login"
	clientAssertion := mustE2EHeadlessClientAssertion(
		t,
		privateKey,
		"headless-login-client",
		receivedAudience,
	)

	logBuffer := &bytes.Buffer{}
	logger := slog.New(slog.NewJSONHandler(logBuffer, &slog.HandlerOptions{Level: slog.LevelDebug}))

	resp, output := runHeadlessPasswordLoginE2ERequest(
		t,
		logger,
		"production",
		jwks,
		clientAssertion,
		"http://sso.hmac.kr/api/v1/auth/headless/password/login",
		nil,
	)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200 for configured public https audience, got %d, body=%s", resp.StatusCode, string(bodyBytes))
	}

	var got map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
		t.Fatalf("failed to decode response body: %v", err)
	}
	if got["redirectTo"] != "http://rp/cb" {
		t.Fatalf("expected redirectTo, got=%v", got["redirectTo"])
	}
	if strings.Contains(output, "\"reason_code\":\"invalid_client_assertion_audience\"") {
		t.Fatalf("did not expect audience mismatch log, got=%s", output)
	}
}

func (m *e2eMockKratosAdminService) CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error) {
	return "", nil
}
