package handler

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/certctl-io/certctl/internal/api/middleware"
)

// fakeAdminCRLCacheService is the test stub for the
// AdminCRLCacheService interface — lets us exercise gate behavior
// (admin / non-admin / explicit-false) without spinning up a real
// CRLCacheRepository or issuer registry.
type fakeAdminCRLCacheService struct {
	called bool
	rows   []CRLCacheRow
	err    error
}

func (f *fakeAdminCRLCacheService) CacheRows(_ context.Context) ([]CRLCacheRow, error) {
	f.called = true
	return f.rows, f.err
}

// TestAdminCRLCache_NonAdmin_Returns403 — M-003-pattern central
// gate test. A caller without an admin-tagged context must be
// rejected with HTTP 403, and the service layer must never see
// the request (no enumeration of issuer set / cache state).
func TestAdminCRLCache_NonAdmin_Returns403(t *testing.T) {
	svc := &fakeAdminCRLCacheService{}
	h := NewAdminCRLCacheHandler(svc)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crl/cache", nil)
	req = req.WithContext(contextWithRequestID()) // request id only, no admin flag
	w := httptest.NewRecorder()

	h.ListCache(w, req)

	if w.Code != http.StatusForbidden {
		t.Fatalf("expected status 403, got %d (body=%q)", w.Code, w.Body.String())
	}
	var resp map[string]any
	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	msg, _ := resp["message"].(string)
	if !strings.Contains(strings.ToLower(msg), "admin") {
		t.Errorf("expected message to mention admin requirement, got %q", msg)
	}
	if svc.called {
		t.Errorf("service was invoked despite non-admin caller — gate failed open")
	}
}

// TestAdminCRLCache_AdminExplicitFalse_Returns403 pins the
// AdminKey-present-but-false case. Without this, a regression to
// "key missing == deny, key present == allow" would silently grant
// a false flag to any caller that managed to set the context value.
func TestAdminCRLCache_AdminExplicitFalse_Returns403(t *testing.T) {
	svc := &fakeAdminCRLCacheService{}
	h := NewAdminCRLCacheHandler(svc)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crl/cache", nil)
	ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
	ctx = context.WithValue(ctx, middleware.AdminKey{}, false)
	req = req.WithContext(ctx)
	w := httptest.NewRecorder()

	h.ListCache(w, req)

	if w.Code != http.StatusForbidden {
		t.Fatalf("expected status 403 for admin=false, got %d", w.Code)
	}
	if svc.called {
		t.Error("service called despite admin=false gate")
	}
}

// TestAdminCRLCache_AdminPermitted_ForwardsActor confirms the
// happy path: an admin-tagged context reaches the service and the
// response shape is what the GUI expects (cache_rows / row_count /
// generated_at). The actor-forwarding aspect of M-002 doesn't apply
// here — this is a read-only endpoint with no audit-event side
// effect — but the test name matches the M008 triplet convention so
// the regression scanner finds it.
func TestAdminCRLCache_AdminPermitted_ForwardsActor(t *testing.T) {
	svc := &fakeAdminCRLCacheService{
		rows: []CRLCacheRow{
			{IssuerID: "iss-a", CachePresent: true, CRLNumber: 1},
			{IssuerID: "iss-b", CachePresent: false},
		},
	}
	h := NewAdminCRLCacheHandler(svc)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crl/cache", nil)
	ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
	ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
	ctx = context.WithValue(ctx, middleware.UserKey{}, "ops-admin")
	req = req.WithContext(ctx)
	w := httptest.NewRecorder()

	h.ListCache(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200 for admin caller, got %d (body=%q)", w.Code, w.Body.String())
	}
	if !svc.called {
		t.Fatal("service was not invoked for admin caller")
	}
	var resp map[string]any
	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if rc, ok := resp["row_count"].(float64); !ok || rc != 2 {
		t.Errorf("row_count = %v, want 2", resp["row_count"])
	}
	if _, ok := resp["cache_rows"].([]any); !ok {
		t.Errorf("cache_rows missing or wrong shape: %v", resp["cache_rows"])
	}
}

// TestAdminCRLCache_RejectsNonGetMethod pins the method gate.
// Companion to the admin gate — both must fire to satisfy the
// admin-only-GET contract.
func TestAdminCRLCache_RejectsNonGetMethod(t *testing.T) {
	h := NewAdminCRLCacheHandler(&fakeAdminCRLCacheService{})

	req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crl/cache", nil)
	ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
	req = req.WithContext(ctx)
	w := httptest.NewRecorder()

	h.ListCache(w, req)

	if w.Code != http.StatusMethodNotAllowed {
		t.Errorf("expected 405 for POST, got %d", w.Code)
	}
}

// TestAdminCRLCache_PropagatesServiceError surfaces 500 when the
// service errors. Pins the failure-path response shape so future
// refactors don't accidentally swallow errors as 200.
func TestAdminCRLCache_PropagatesServiceError(t *testing.T) {
	svc := &fakeAdminCRLCacheService{err: errors.New("db down")}
	h := NewAdminCRLCacheHandler(svc)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crl/cache", nil)
	ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
	req = req.WithContext(ctx)
	w := httptest.NewRecorder()

	h.ListCache(w, req)

	if w.Code != http.StatusInternalServerError {
		t.Errorf("expected 500 on service error, got %d", w.Code)
	}
}
