package handler

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

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

func TestRevokeLinkedRp_Success(t *testing.T) {
	// Mock Hydra transport for revocation
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		// 1. Kratos whoami
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"identity": map[string]any{"id": "user-123"},
			}), nil
		}
		// 2. Hydra Revoke
		if r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
			assert.Equal(t, "user-123", r.URL.Query().Get("subject"))
			assert.Equal(t, "app-1", r.URL.Query().Get("client"))
			return httpResponse(r, http.StatusNoContent, ""), nil
		}
		return httpResponse(r, http.StatusNotFound, "not found"), nil
	})

	client := &http.Client{Transport: transport}
	origDefault := http.DefaultClient
	http.DefaultClient = client
	defer func() { http.DefaultClient = origDefault }()

	auditRepo := &mockAuditRepo{}
	rpUsageSink := &mockRPUsageEventSink{}
	consentRepo := &mockConsentRepo{
		consents: []domain.ClientConsent{
			{
				ClientID:      "app-1",
				Subject:       "user-123",
				GrantedScopes: []string{"openid", "profile"},
			},
		},
	}
	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		AuditRepo:   auditRepo,
		ConsentRepo: consentRepo,
		RPUsageSink: rpUsageSink,
	}
	app := fiber.New()
	app.Delete("/api/v1/user/rp/linked/:id", h.RevokeLinkedRp)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/rp/linked/app-1", nil)
	req.Header.Set("Cookie", "valid")

	resp, _ := app.Test(req, -1)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, 1, len(auditRepo.logs))
	assert.Equal(t, "consent.revoked", auditRepo.logs[0].EventType)
	assert.Equal(t, "user-123", auditRepo.logs[0].UserID)
	assert.Equal(t, "success", auditRepo.logs[0].Status)
	auditDetails, err := utils.ParseAuditDetails(auditRepo.logs[0].Details)
	assert.NoError(t, err)
	assert.Equal(t, "app-1", auditDetails["client_id"])
	assert.Equal(t, 1, len(rpUsageSink.events))
	assert.Equal(t, domain.RPUsageEventTypeAuthorizationRevoked, rpUsageSink.events[0].EventType)
	assert.Equal(t, "user-123", rpUsageSink.events[0].Subject)
	assert.Equal(t, "app-1", rpUsageSink.events[0].ClientID)
	remaining, err := consentRepo.Find(req.Context(), "app-1", "user-123")
	assert.NoError(t, err)
	assert.Nil(t, remaining)
}

func TestRevokeLinkedRp_SendsBackchannelLogoutTokenWhenConfigured(t *testing.T) {
	t.Setenv("BACKCHANNEL_LOGOUT_ISSUER", "https://sso.example.com/oidc")

	var receivedBody string
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.URL.Path == "/sessions/whoami" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"identity": map[string]any{"id": "user-123"},
			}), nil
		}
		if r.URL.Host == "hydra.test" && r.Method == http.MethodDelete && r.URL.Path == "/oauth2/auth/sessions/consent" {
			return httpResponse(r, http.StatusNoContent, ""), nil
		}
		if r.URL.Host == "hydra.test" && r.Method == http.MethodGet && r.URL.Path == "/clients/app-1" {
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"client_id":              "app-1",
				"backchannel_logout_uri": "https://rp.example.com/backchannel-logout",
			}), nil
		}
		if r.URL.Host == "rp.example.com" && 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
	})

	client := &http.Client{Transport: transport}
	origDefault := http.DefaultClient
	http.DefaultClient = client
	defer func() { http.DefaultClient = origDefault }()

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

	auditRepo := &mockAuditRepo{}
	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		BackchannelLogout: backchannelLogout,
		AuditRepo:         auditRepo,
	}
	app := fiber.New()
	app.Delete("/api/v1/user/rp/linked/:id", h.RevokeLinkedRp)

	req := httptest.NewRequest(http.MethodDelete, "/api/v1/user/rp/linked/app-1", nil)
	req.Header.Set("Cookie", "valid")

	resp, _ := app.Test(req, -1)
	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"))

	assert.Len(t, auditRepo.logs, 2)
	assert.Equal(t, "backchannel_logout.sent", auditRepo.logs[1].EventType)
}

func TestListRpHistory_Aggregation(t *testing.T) {
	now := time.Now()
	auditRepo := &mockAuditRepo{
		logs: []domain.AuditLog{
			{
				UserID:    "user-123",
				EventType: "consent.revoked", // Newest
				Timestamp: now,
				Details:   `{"client_id":"app-1"}`,
			},
			{
				UserID:    "user-123",
				EventType: "consent.granted", // Oldest
				Timestamp: now.Add(-1 * time.Hour),
				Details:   `{"client_id":"app-1", "client_name":"App One"}`,
			},
		},
	}

	h := &AuthHandler{
		AuditRepo: auditRepo,
	}
	app := fiber.New()
	app.Get("/api/v1/user/rp/history", h.ListRpHistory)

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		return httpJSONAny(r, http.StatusOK, map[string]any{
			"identity": map[string]any{"id": "user-123"},
		}), nil
	})
	http.DefaultClient = &http.Client{Transport: transport}

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

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

	var res struct {
		Items []struct {
			ClientID string `json:"client_id"`
			Status   string `json:"status"`
		} `json:"items"`
	}
	json.NewDecoder(resp.Body).Decode(&res)

	assert.Equal(t, 1, len(res.Items))
	assert.Equal(t, "app-1", res.Items[0].ClientID)
	// Newest event (revoked) should win
	assert.Equal(t, "revoked", res.Items[0].Status)
}
