package service

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"strings"
	"testing"

	"github.com/certctl-io/certctl/internal/domain"
)

// EST RFC 7030 hardening master bundle Phase 11.4 — audit-code assertions.
// Drive each code path through a real ESTService instance + assert the
// typed action codes land in the audit log alongside the legacy bare
// codes (back-compat preservation).

func newAuditAssertService(t *testing.T) (*ESTService, *mockAuditRepo) {
	t.Helper()
	auditRepo := newMockAuditRepository()
	auditSvc := NewAuditService(auditRepo)
	silent := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError + 10}))
	svc := NewESTService("iss-corp", &mockIssuerConnector{}, auditSvc, silent)
	return svc, auditRepo
}

// auditActions returns the action codes recorded across every audit
// event in the repo, in emission order. Used to assert that the
// typed _success / _failed events fire in the right order alongside
// the legacy bare codes.
func auditActions(repo *mockAuditRepo) []string {
	out := make([]string, 0, len(repo.Events))
	for _, e := range repo.Events {
		out = append(out, e.Action)
	}
	return out
}

func TestESTAudit_SimpleEnrollSuccess_EmitsLegacyAndTyped(t *testing.T) {
	svc, repo := newAuditAssertService(t)
	csrPEM := generateCSRPEM(t, "device.example.com", []string{"device.example.com"})
	if _, err := svc.SimpleEnroll(context.Background(), csrPEM); err != nil {
		t.Fatalf("SimpleEnroll: %v", err)
	}
	got := auditActions(repo)
	wantBare := "est_simple_enroll"
	wantTyped := AuditActionESTSimpleEnrollSuccess // est_simple_enroll_success
	if !stringSliceContains(got, wantBare) {
		t.Errorf("missing legacy bare code %q in %v", wantBare, got)
	}
	if !stringSliceContains(got, wantTyped) {
		t.Errorf("missing typed code %q in %v", wantTyped, got)
	}
}

func TestESTAudit_SimpleReEnrollSuccess_EmitsTyped(t *testing.T) {
	svc, repo := newAuditAssertService(t)
	csrPEM := generateCSRPEM(t, "device.example.com", nil)
	if _, err := svc.SimpleReEnroll(context.Background(), csrPEM); err != nil {
		t.Fatalf("SimpleReEnroll: %v", err)
	}
	if !stringSliceContains(auditActions(repo), AuditActionESTSimpleReEnrollSuccess) {
		t.Errorf("missing %q; got %v", AuditActionESTSimpleReEnrollSuccess, auditActions(repo))
	}
}

func TestESTAudit_IssuerError_EmitsTypedFailed(t *testing.T) {
	auditRepo := newMockAuditRepository()
	auditSvc := NewAuditService(auditRepo)
	silent := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError + 10}))
	svc := NewESTService("iss-corp", &mockIssuerConnector{Err: errors.New("CA down")}, auditSvc, silent)
	csrPEM := generateCSRPEM(t, "device.example.com", nil)
	if _, err := svc.SimpleEnroll(context.Background(), csrPEM); err == nil {
		t.Fatal("expected enroll error")
	}
	if !stringSliceContains(auditActions(auditRepo), AuditActionESTSimpleEnrollFailed) {
		t.Errorf("missing typed failure code; got %v", auditActions(auditRepo))
	}
	// And the bare _failed variant for back-compat:
	if !stringSliceContains(auditActions(auditRepo), "est_simple_enroll_failed") {
		t.Errorf("missing bare _failed variant; got %v", auditActions(auditRepo))
	}
}

func TestESTAudit_PolicyViolation_EmitsTypedAndStandalone(t *testing.T) {
	svc, repo := newAuditAssertService(t)
	repoMock := newMockProfileRepository()
	svc.SetProfileRepo(repoMock)
	svc.SetProfileID("prof-tight")
	repoMock.AddProfile(&domain.CertificateProfile{
		ID:                   "prof-tight",
		Name:                 "tight",
		AllowedKeyAlgorithms: []domain.KeyAlgorithmRule{{Algorithm: "RSA", MinSize: 4096}}, // ECDSA-P256 CSR fails
		Enabled:              true,
	})
	csrPEM := generateCSRPEM(t, "device.example.com", nil) // ECDSA-P256
	if _, err := svc.SimpleEnroll(context.Background(), csrPEM); err == nil {
		t.Fatal("expected policy violation error")
	}
	got := auditActions(repo)
	if !stringSliceContains(got, AuditActionESTCSRPolicyViolation) {
		t.Errorf("missing standalone policy-violation code %q; got %v", AuditActionESTCSRPolicyViolation, got)
	}
	if !stringSliceContains(got, AuditActionESTSimpleEnrollFailed) {
		t.Errorf("missing typed failed code; got %v", got)
	}
}

func TestESTAudit_AuditCodesAreUniqueStrings(t *testing.T) {
	// Tiny invariant test: every audit-action constant is a non-empty
	// distinct string. Prevents a future cut-paste typo where two
	// constants share the same value.
	codes := []string{
		AuditActionESTSimpleEnrollSuccess,
		AuditActionESTSimpleEnrollFailed,
		AuditActionESTSimpleReEnrollSuccess,
		AuditActionESTSimpleReEnrollFailed,
		AuditActionESTServerKeygenSuccess,
		AuditActionESTServerKeygenFailed,
		AuditActionESTAuthFailedBasic,
		AuditActionESTAuthFailedMTLS,
		AuditActionESTAuthFailedChannelBinding,
		AuditActionESTRateLimited,
		AuditActionESTCSRPolicyViolation,
		AuditActionESTBulkRevoke,
		AuditActionESTTrustAnchorReloaded,
	}
	seen := map[string]bool{}
	for _, c := range codes {
		if c == "" {
			t.Errorf("empty audit-action constant")
		}
		if !strings.HasPrefix(c, "est_") {
			t.Errorf("audit-action constant %q must start with est_", c)
		}
		if seen[c] {
			t.Errorf("duplicate audit-action constant: %q", c)
		}
		seen[c] = true
	}
}

func stringSliceContains(haystack []string, needle string) bool {
	for _, s := range haystack {
		if s == needle {
			return true
		}
	}
	return false
}

// silenceUnusedDomain keeps the domain import live when the policy-
// violation test compiles even if a future refactor removes the only
// reference site.
var _ domain.CertificateProfile
