package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/service"
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func TestListMySessions_Success(t *testing.T) {
	now := time.Date(2026, 4, 2, 1, 2, 3, 0, time.UTC)
	setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id":               "current-sid",
				"authenticated_at": now.Format(time.RFC3339),
				"identity": map[string]any{
					"id": "user-123",
					"traits": map[string]any{
						"email": "user@example.com",
						"name":  "User",
						"role":  "user",
					},
				},
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	}))

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{
			ID:              "current-sid",
			Active:          true,
			AuthenticatedAt: now,
			ExpiresAt:       now.Add(24 * time.Hour),
		},
		{
			ID:              "other-sid",
			Active:          true,
			AuthenticatedAt: now.Add(-2 * time.Hour),
			ExpiresAt:       now.Add(22 * time.Hour),
		},
	}, nil).Once()

	auditRepo := &mockAuditRepo{
		logs: []domain.AuditLog{
			{
				UserID:    "user-123",
				EventType: "login_success",
				SessionID: "other-sid",
				Timestamp: now.Add(-30 * time.Minute),
				IPAddress: "203.0.113.10",
				UserAgent: "Mozilla/5.0",
			},
		},
	}

	h := &AuthHandler{
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
	}

	app := fiber.New()
	app.Get("/api/v1/user/sessions", h.ListMySessions)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body struct {
		Items []struct {
			SessionID string `json:"session_id"`
			IsCurrent bool   `json:"is_current"`
			IsActive  bool   `json:"is_active"`
			IPAddress string `json:"ip_address"`
			UserAgent string `json:"user_agent"`
		} `json:"items"`
	}
	err = json.NewDecoder(resp.Body).Decode(&body)
	assert.NoError(t, err)
	if assert.Len(t, body.Items, 2) {
		assert.Equal(t, "current-sid", body.Items[0].SessionID)
		assert.True(t, body.Items[0].IsCurrent)
		assert.Equal(t, "other-sid", body.Items[1].SessionID)
		assert.True(t, body.Items[1].IsActive)
		assert.Equal(t, "203.0.113.10", body.Items[1].IPAddress)
		assert.Equal(t, "Mozilla/5.0", body.Items[1].UserAgent)
	}

	mockKratos.AssertExpectations(t)
}

func TestListMySessions_UsesConsentGrantForAppName(t *testing.T) {
	now := time.Date(2026, 4, 2, 4, 40, 0, 0, time.UTC)
	setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id":               "current-sid",
				"authenticated_at": now.Format(time.RFC3339),
				"identity": map[string]any{
					"id": "user-123",
					"traits": map[string]any{
						"email": "user@example.com",
						"name":  "User",
						"role":  "user",
					},
				},
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	}))

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{
			ID:              "current-sid",
			Active:          true,
			AuthenticatedAt: now,
			ExpiresAt:       now.Add(24 * time.Hour),
		},
		{
			ID:              "c7c721ea-session",
			Active:          true,
			AuthenticatedAt: now.Add(-5 * time.Minute),
			ExpiresAt:       now.Add(23*time.Hour + 55*time.Minute),
		},
	}, nil).Once()

	auditRepo := &mockAuditRepo{
		logs: []domain.AuditLog{
			{
				UserID:    "user-123",
				EventType: "consent.granted",
				SessionID: "c7c721ea-session",
				Timestamp: now,
				Details:   `{"client_id":"devfront","client_name":"DevFront","session_id":"c7c721ea-session","approved_session_id":"c7c721ea-session"}`,
			},
		},
	}

	h := &AuthHandler{
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
	}

	app := fiber.New()
	app.Get("/api/v1/user/sessions", h.ListMySessions)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body struct {
		Items []struct {
			SessionID string `json:"session_id"`
			AppName   string `json:"app_name"`
			ClientID  string `json:"client_id"`
		} `json:"items"`
	}
	err = json.NewDecoder(resp.Body).Decode(&body)
	assert.NoError(t, err)
	if assert.Len(t, body.Items, 2) {
		assert.Equal(t, "c7c721ea-session", body.Items[1].SessionID)
		assert.Equal(t, "DevFront", body.Items[1].AppName)
		assert.Equal(t, "devfront", body.Items[1].ClientID)
	}

	mockKratos.AssertExpectations(t)
}

func TestListMySessions_PreservesAppNameFromOlderConsentGrant(t *testing.T) {
	now := time.Date(2026, 4, 2, 4, 40, 0, 0, time.UTC)
	setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id":               "current-sid",
				"authenticated_at": now.Format(time.RFC3339),
				"identity": map[string]any{
					"id": "user-123",
					"traits": map[string]any{
						"email": "user@example.com",
						"name":  "User",
						"role":  "user",
					},
				},
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	}))

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{
			ID:              "current-sid",
			Active:          true,
			AuthenticatedAt: now,
			ExpiresAt:       now.Add(24 * time.Hour),
		},
		{
			ID:              "c7c721ea-session",
			Active:          true,
			AuthenticatedAt: now.Add(-5 * time.Minute),
			ExpiresAt:       now.Add(23*time.Hour + 55*time.Minute),
		},
	}, nil).Once()

	auditRepo := &mockAuditRepo{
		logs: []domain.AuditLog{
			{
				UserID:    "user-123",
				EventType: "consent.granted",
				SessionID: "c7c721ea-session",
				Timestamp: now.Add(-30 * time.Second),
				IPAddress: "203.0.113.10",
				Details:   `{"client_id":"devfront","client_name":"DevFront","session_id":"c7c721ea-session"}`,
			},
			{
				UserID:    "user-123",
				EventType: "login_success",
				SessionID: "c7c721ea-session",
				Timestamp: now,
				IPAddress: "10.0.0.12",
				UserAgent: "Mozilla/5.0",
			},
		},
	}

	h := &AuthHandler{
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
	}

	app := fiber.New()
	app.Get("/api/v1/user/sessions", h.ListMySessions)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body struct {
		Items []struct {
			SessionID string `json:"session_id"`
			AppName   string `json:"app_name"`
			ClientID  string `json:"client_id"`
			IPAddress string `json:"ip_address"`
		} `json:"items"`
	}
	err = json.NewDecoder(resp.Body).Decode(&body)
	assert.NoError(t, err)
	if assert.Len(t, body.Items, 2) {
		assert.Equal(t, "c7c721ea-session", body.Items[1].SessionID)
		assert.Equal(t, "DevFront", body.Items[1].AppName)
		assert.Equal(t, "devfront", body.Items[1].ClientID)
		assert.Equal(t, "203.0.113.10", body.Items[1].IPAddress)
	}

	mockKratos.AssertExpectations(t)
}

func TestListMySessions_CurrentSessionFallsBackToRequestMetadata(t *testing.T) {
	now := time.Date(2026, 4, 6, 1, 2, 3, 0, time.UTC)
	setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id":               "current-sid",
				"authenticated_at": now.Format(time.RFC3339),
				"identity": map[string]any{
					"id": "user-123",
					"traits": map[string]any{
						"email": "user@example.com",
						"name":  "User",
						"role":  "user",
					},
				},
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	}))

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{
			ID:              "current-sid",
			Active:          true,
			AuthenticatedAt: now,
			ExpiresAt:       now.Add(24 * time.Hour),
		},
	}, nil).Once()

	h := &AuthHandler{
		KratosAdmin: mockKratos,
		AuditRepo:   &mockAuditRepo{},
	}

	app := fiber.New()
	app.Get("/api/v1/user/sessions", h.ListMySessions)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")
	req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/146.0.0.0 Safari/537.36")
	req.Header.Set("X-Forwarded-For", "100.100.100.1, 203.0.113.25")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body struct {
		Items []struct {
			SessionID string `json:"session_id"`
			IsCurrent bool   `json:"is_current"`
			IPAddress string `json:"ip_address"`
			UserAgent string `json:"user_agent"`
			ClientID  string `json:"client_id"`
			AppName   string `json:"app_name"`
		} `json:"items"`
	}
	err = json.NewDecoder(resp.Body).Decode(&body)
	assert.NoError(t, err)
	if assert.Len(t, body.Items, 1) {
		assert.Equal(t, "current-sid", body.Items[0].SessionID)
		assert.True(t, body.Items[0].IsCurrent)
		assert.Equal(t, "203.0.113.25", body.Items[0].IPAddress)
		assert.Contains(t, body.Items[0].UserAgent, "Mozilla/5.0")
		assert.Equal(t, "userfront", body.Items[0].ClientID)
		assert.Equal(t, "UserFront", body.Items[0].AppName)
	}

	mockKratos.AssertExpectations(t)
}

func TestDeleteMySession_Success(t *testing.T) {
	t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
	var hydraRevokeCalls int
	client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
		switch r.URL.Host {
		case "kratos.test":
			if r.URL.Path == "/sessions/whoami" {
				return httpJSONAny(r, http.StatusOK, map[string]any{
					"id":               "current-sid",
					"authenticated_at": time.Now().UTC().Format(time.RFC3339),
					"identity": map[string]any{
						"id": "user-123",
						"traits": map[string]any{
							"email": "user@example.com",
							"name":  "User",
							"role":  "user",
						},
					},
				}), nil
			}
		case "hydra.test":
			if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
				if r.URL.Query().Get("subject") != "user-123" {
					t.Fatalf("unexpected revoke subject: %s", r.URL.Query().Get("subject"))
				}
				if r.URL.Query().Get("client") != "devfront" {
					t.Fatalf("unexpected revoke client: %s", r.URL.Query().Get("client"))
				}
				hydraRevokeCalls++
				return httpResponse(r, http.StatusNoContent, ""), nil
			}
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})}
	setDefaultHTTPClientForTest(t, client.Transport)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{ID: "target-sid", Active: true},
	}, nil).Once()
	mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{
		ID:     "target-sid",
		Active: true,
	}, nil).Once()
	mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once()

	auditRepo := &mockAuditRepo{}
	h := &AuthHandler{
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
	}
	auditRepo.logs = append(auditRepo.logs, domain.AuditLog{
		UserID:    "user-123",
		EventType: "POST /api/v1/auth/oidc/login/accept",
		SessionID: "target-sid",
		Details:   `{"client_id":"devfront","client_name":"Devfront"}`,
	})

	app := fiber.New()
	app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")
	req.Header.Set("User-Agent", "session-test-agent")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	if assert.Len(t, auditRepo.logs, 2) {
		assert.Equal(t, "session.revoked", auditRepo.logs[len(auditRepo.logs)-1].EventType)
		assert.Equal(t, "user-123", auditRepo.logs[len(auditRepo.logs)-1].UserID)
		assert.Equal(t, "current-sid", auditRepo.logs[len(auditRepo.logs)-1].SessionID)
		assert.Contains(t, auditRepo.logs[len(auditRepo.logs)-1].Details, "target-sid")
	}
	assert.Equal(t, 1, hydraRevokeCalls)

	mockKratos.AssertExpectations(t)
}

func TestDeleteMySession_DoesNotRevokeAllHydraSessionsWhenClientBindingMissing(t *testing.T) {
	t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
	var hydraRevokeCalls int
	client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
		switch r.URL.Host {
		case "kratos.test":
			if r.URL.Path == "/sessions/whoami" {
				return httpJSONAny(r, http.StatusOK, map[string]any{
					"id":               "current-sid",
					"authenticated_at": time.Now().UTC().Format(time.RFC3339),
					"identity": map[string]any{
						"id": "user-123",
						"traits": map[string]any{
							"email": "user@example.com",
							"name":  "User",
							"role":  "user",
						},
					},
				}), nil
			}
		case "hydra.test":
			if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
				hydraRevokeCalls++
				return httpResponse(r, http.StatusNoContent, ""), nil
			}
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})}
	setDefaultHTTPClientForTest(t, client.Transport)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{ID: "target-sid", Active: true},
	}, nil).Once()
	mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{
		ID:     "target-sid",
		Active: true,
	}, nil).Once()
	mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once()

	auditRepo := &mockAuditRepo{}
	h := &AuthHandler{
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
	}

	app := fiber.New()
	app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")
	req.Header.Set("User-Agent", "session-test-agent")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, 0, hydraRevokeCalls)
	if assert.Len(t, auditRepo.logs, 1) {
		assert.Equal(t, "session.revoked", auditRepo.logs[0].EventType)
		assert.Equal(t, "user-123", auditRepo.logs[0].UserID)
		assert.Contains(t, auditRepo.logs[0].Details, "target-sid")
	}

	mockKratos.AssertExpectations(t)
}

func TestDeleteMySession_SendsBackchannelLogoutTokenWhenClientConfigured(t *testing.T) {
	t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
	t.Setenv("BACKCHANNEL_LOGOUT_ISSUER", "https://sso.example.com/oidc")

	var receivedBody string
	client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
		switch r.URL.Host {
		case "kratos.test":
			if r.URL.Path == "/sessions/whoami" {
				return httpJSONAny(r, http.StatusOK, map[string]any{
					"id":               "current-sid",
					"authenticated_at": time.Now().UTC().Format(time.RFC3339),
					"identity": map[string]any{
						"id": "user-123",
						"traits": map[string]any{
							"email": "user@example.com",
							"name":  "User",
							"role":  "user",
						},
					},
				}), nil
			}
		case "hydra.test":
			if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
				return httpResponse(r, http.StatusNoContent, ""), nil
			}
			if r.Method == http.MethodGet && r.URL.Path == "/clients/devfront" {
				return httpJSONAny(r, http.StatusOK, map[string]any{
					"client_id":              "devfront",
					"backchannel_logout_uri": "https://rp.example.com/backchannel-logout",
				}), nil
			}
		case "rp.example.com":
			if r.Method == http.MethodPost && r.URL.Path == "/backchannel-logout" {
				raw, _ := io.ReadAll(r.Body)
				receivedBody = string(raw)
				return httpResponse(r, http.StatusNoContent, ""), nil
			}
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})}
	setDefaultHTTPClientForTest(t, client.Transport)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{ID: "target-sid", Active: true},
	}, nil).Once()
	mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{
		ID:     "target-sid",
		Active: true,
	}, nil).Once()
	mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once()

	backchannelLogout, err := service.NewBackchannelLogoutService()
	assert.NoError(t, err)
	backchannelLogout.HTTPClient = client

	auditRepo := &mockAuditRepo{}
	h := &AuthHandler{
		KratosAdmin:       mockKratos,
		AuditRepo:         auditRepo,
		BackchannelLogout: backchannelLogout,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
	}
	auditRepo.logs = append(auditRepo.logs, domain.AuditLog{
		UserID:    "user-123",
		EventType: "POST /api/v1/auth/oidc/login/accept",
		SessionID: "target-sid",
		Details:   `{"client_id":"devfront","client_name":"Devfront"}`,
	})

	app := fiber.New()
	app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")
	req.Header.Set("User-Agent", "session-test-agent")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.True(t, strings.Contains(receivedBody, "logout_token="))

	values, err := url.ParseQuery(receivedBody)
	assert.NoError(t, err)
	assert.NotEmpty(t, values.Get("logout_token"))

	foundBackchannelAudit := false
	for _, log := range auditRepo.logs {
		if log.EventType == "backchannel_logout.sent" {
			foundBackchannelAudit = true
			break
		}
	}
	assert.True(t, foundBackchannelAudit)

	mockKratos.AssertExpectations(t)
}

func TestDeleteMySession_RevokesHydraClientBoundFromPasswordLoginAudit(t *testing.T) {
	t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
	var hydraRevokeCalls int
	var revokedClient string
	client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
		switch r.URL.Host {
		case "kratos.test":
			if r.URL.Path == "/sessions/whoami" {
				return httpJSONAny(r, http.StatusOK, map[string]any{
					"id":               "current-sid",
					"authenticated_at": time.Now().UTC().Format(time.RFC3339),
					"identity": map[string]any{
						"id": "user-123",
						"traits": map[string]any{
							"email": "user@example.com",
							"name":  "User",
							"role":  "user",
						},
					},
				}), nil
			}
		case "hydra.test":
			if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
				revokedClient = r.URL.Query().Get("client")
				hydraRevokeCalls++
				return httpResponse(r, http.StatusNoContent, ""), nil
			}
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})}
	setDefaultHTTPClientForTest(t, client.Transport)

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{ID: "target-sid", Active: true},
	}, nil).Once()
	mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{
		ID:     "target-sid",
		Active: true,
	}, nil).Once()
	mockKratos.On("DeleteSession", mock.Anything, "target-sid").Return(nil).Once()

	auditRepo := &mockAuditRepo{}
	h := &AuthHandler{
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
	}
	auditRepo.logs = append(auditRepo.logs, domain.AuditLog{
		UserID:    "user-123",
		EventType: "POST /api/v1/auth/password/login",
		SessionID: "target-sid",
		Details:   `{"client_id":"adminfront","client_name":"AdminFront","session_id":"target-sid"}`,
	})

	app := fiber.New()
	app.Delete("/api/v1/user/sessions/:id", h.DeleteMySession)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/sessions/target-sid", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")
	req.Header.Set("User-Agent", "session-test-agent")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, 1, hydraRevokeCalls)
	assert.Equal(t, "adminfront", revokedClient)

	mockKratos.AssertExpectations(t)
}

func TestGetHydraProfile_RejectsInactiveLinkedSession(t *testing.T) {
	client := &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Host == "hydra.test" && r.URL.Path == "/oauth2/introspect" {
			body, _ := io.ReadAll(r.Body)
			if string(body) != "token=opaque-token" {
				t.Fatalf("unexpected introspect body: %s", string(body))
			}
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"active":    true,
				"sub":       "user-123",
				"client_id": "devfront",
				"ext": map[string]any{
					"session_id": "target-sid",
				},
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})}

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("GetSession", mock.Anything, "target-sid").Return(&service.KratosSession{
		ID:     "target-sid",
		Active: false,
		Identity: &service.KratosIdentity{
			ID: "user-123",
		},
	}, nil).Once()

	h := &AuthHandler{
		KratosAdmin: mockKratos,
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
	}

	profile, err := h.getHydraProfile(context.Background(), "opaque-token")
	assert.Nil(t, profile)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "inactive")
	mockKratos.AssertExpectations(t)
}

func TestGetAuthTimeline_FillsSessionIDFromOathkeeperRaw(t *testing.T) {
	now := time.Date(2026, 4, 7, 4, 39, 0, 0, time.UTC)
	setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id":               "current-sid",
				"authenticated_at": now.Format(time.RFC3339),
				"identity": map[string]any{
					"id": "user-123",
					"traits": map[string]any{
						"email": "user@example.com",
						"name":  "User",
						"role":  "user",
					},
				},
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	}))

	h := &AuthHandler{
		AuditRepo: &mockAuditRepo{},
		OathkeeperRepo: &mockOathkeeperRepo{
			logs: []domain.OathkeeperAccessLog{
				{
					Timestamp: now,
					RequestID: "req-1",
					Method:    http.MethodGet,
					Path:      "/api/v1/dev/sessions",
					Status:    http.StatusOK,
					Subject:   "user-123",
					ClientIP:  "203.0.113.7",
					UserAgent: "Mozilla/5.0",
					Raw:       `{"request":{"url":"https://devfront.example.com/callback?client_id=devfront"},"extra":{"session_id":"target-sid"}}`,
				},
			},
		},
	}

	app := fiber.New()
	app.Get("/api/v1/audit/auth/timeline", h.GetAuthTimeline)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/audit/auth/timeline", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body struct {
		Items []struct {
			SessionID string `json:"session_id"`
			ClientID  string `json:"client_id"`
			AppName   string `json:"app_name"`
			Source    string `json:"source"`
		} `json:"items"`
	}
	err = json.NewDecoder(resp.Body).Decode(&body)
	assert.NoError(t, err)
	if assert.Len(t, body.Items, 1) {
		assert.Equal(t, "target-sid", body.Items[0].SessionID)
		assert.Equal(t, "devfront", body.Items[0].ClientID)
		assert.Equal(t, "devfront", body.Items[0].AppName)
		assert.Equal(t, "oathkeeper", body.Items[0].Source)
	}
}

func TestGetAuthTimeline_IncludesHeadlessPasswordLogin(t *testing.T) {
	now := time.Date(2026, 4, 7, 5, 10, 0, 0, time.UTC)
	setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id":               "current-sid",
				"authenticated_at": now.Format(time.RFC3339),
				"identity": map[string]any{
					"id": "user-123",
					"traits": map[string]any{
						"email": "user@example.com",
						"name":  "User",
						"role":  "user",
					},
				},
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	}))

	h := &AuthHandler{
		AuditRepo: &mockAuditRepo{
			logs: []domain.AuditLog{
				{
					EventID:   "audit-1",
					Timestamp: now,
					UserID:    "user-123",
					SessionID: "headless-session-1",
					EventType: "POST /api/v1/auth/headless/password/login",
					Status:    "success",
					IPAddress: "203.0.113.20",
					UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/146.0.0.0 Safari/537.36",
					Details:   `{"client_id":"headless-login-client","client_name":"Headless Login Portal","session_id":"headless-session-1","login_id":"user@example.com","login_challenge":"challenge-123"}`,
				},
			},
		},
	}

	app := fiber.New()
	app.Get("/api/v1/audit/auth/timeline", h.GetAuthTimeline)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/audit/auth/timeline", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body struct {
		Items []struct {
			SessionID  string `json:"session_id"`
			ClientID   string `json:"client_id"`
			AppName    string `json:"app_name"`
			AuthMethod string `json:"auth_method"`
			EventType  string `json:"event_type"`
		} `json:"items"`
	}
	err = json.NewDecoder(resp.Body).Decode(&body)
	assert.NoError(t, err)
	if assert.Len(t, body.Items, 1) {
		assert.Equal(t, "headless-session-1", body.Items[0].SessionID)
		assert.Equal(t, "headless-login-client", body.Items[0].ClientID)
		assert.Equal(t, "Headless Login Portal", body.Items[0].AppName)
		assert.Equal(t, "비밀번호(Email)", body.Items[0].AuthMethod)
		assert.Equal(t, "POST /api/v1/auth/headless/password/login", body.Items[0].EventType)
	}
}

func TestListMySessions_UsesHeadlessPasswordLoginForClientBinding(t *testing.T) {
	now := time.Date(2026, 4, 7, 5, 35, 0, 0, time.UTC)
	setDefaultHTTPClientForTest(t, roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"id":               "current-sid",
				"authenticated_at": now.Format(time.RFC3339),
				"identity": map[string]any{
					"id": "user-123",
					"traits": map[string]any{
						"email": "user@example.com",
						"name":  "User",
						"role":  "user",
					},
				},
			}), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	}))

	mockKratos := new(MockKratosAdminService)
	mockKratos.On("ListIdentitySessions", mock.Anything, "user-123").Return([]service.KratosSession{
		{
			ID:              "current-sid",
			Active:          true,
			AuthenticatedAt: now,
			ExpiresAt:       now.Add(24 * time.Hour),
		},
		{
			ID:              "headless-session-1",
			Active:          true,
			AuthenticatedAt: now.Add(-10 * time.Minute),
			ExpiresAt:       now.Add(23*time.Hour + 50*time.Minute),
		},
	}, nil).Once()

	auditRepo := &mockAuditRepo{
		logs: []domain.AuditLog{
			{
				UserID:    "user-123",
				EventType: "POST /api/v1/auth/headless/password/login",
				SessionID: "headless-session-1",
				Timestamp: now,
				IPAddress: "203.0.113.20",
				UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/146.0.0.0 Safari/537.36",
				Details:   `{"client_id":"headless-login-client","client_name":"Headless Login Portal","session_id":"headless-session-1"}`,
			},
		},
	}

	h := &AuthHandler{
		KratosAdmin: mockKratos,
		AuditRepo:   auditRepo,
	}

	app := fiber.New()
	app.Get("/api/v1/user/sessions", h.ListMySessions)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/user/sessions", nil)
	req.Header.Set("Cookie", "ory_kratos_session=valid")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	var body struct {
		Items []struct {
			SessionID string `json:"session_id"`
			AppName   string `json:"app_name"`
			ClientID  string `json:"client_id"`
			IPAddress string `json:"ip_address"`
			UserAgent string `json:"user_agent"`
		} `json:"items"`
	}
	err = json.NewDecoder(resp.Body).Decode(&body)
	assert.NoError(t, err)
	if assert.Len(t, body.Items, 2) {
		assert.Equal(t, "headless-session-1", body.Items[1].SessionID)
		assert.Equal(t, "Headless Login Portal", body.Items[1].AppName)
		assert.Equal(t, "headless-login-client", body.Items[1].ClientID)
		assert.Equal(t, "203.0.113.20", body.Items[1].IPAddress)
		assert.Equal(t, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/146.0.0.0 Safari/537.36", body.Items[1].UserAgent)
	}

	mockKratos.AssertExpectations(t)
}
