package service

import (
	"context"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"errors"
	"math/big"
	"testing"
	"time"

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

// SCEP RFC 8894 + Intune master prompt §13 line 1859 acceptance —
// coverage uplift on the SCEP probe persistence + clamp paths. Closed
// in the 2026-04-29 audit-closure bundle (Phase H).
//
// Targets the lowest-coverage hot spots in
// internal/service/scep_probe.go (per the audit) without bloating the
// suite:
//
//   1. persistProbeResult is nil-safe + nil-repo-safe.
//   2. persistProbeResult swallows repo errors (probe stays a "best-
//      effort persist") + still surfaces them through the logger.
//   3. ListRecentSCEPProbes returns an empty slice (NOT nil) when no
//      repo is wired so JSON marshaling stays clean.
//   4. describeCertAlgorithm covers RSA/ECDSA/Ed25519/unknown branches
//      including the QF1008 nil-curve defensive branch added in
//      commit 9fcea95.

// stubSCEPProbeRepo is a controllable repository.SCEPProbeResultRepository
// used by the persist + list tests. Returns the configured insertErr +
// listResults from each Insert/ListRecent call; bumps insertCalls so the
// test can assert which probes reached the persist path.
type stubSCEPProbeRepo struct {
	insertCalls int
	insertErr   error
	listResults []*domain.SCEPProbeResult
	listLimit   int
	listErr     error
}

func (r *stubSCEPProbeRepo) Insert(_ context.Context, _ *domain.SCEPProbeResult) error {
	r.insertCalls++
	return r.insertErr
}

func (r *stubSCEPProbeRepo) ListRecent(_ context.Context, limit int) ([]*domain.SCEPProbeResult, error) {
	r.listLimit = limit
	return r.listResults, r.listErr
}

// TestPersistProbeResult_NoRepoIsNoOp verifies persistProbeResult is
// safe to call before SetSCEPProbeRepo wires a repo (the production
// startup order is: build service → wire repo). Without this, a probe
// that runs during the boot window would nil-deref.
func TestPersistProbeResult_NoRepoIsNoOp(t *testing.T) {
	s := newScepProbeServiceForTest(t)
	// Should not panic even though scepProbeRepo is nil.
	s.persistProbeResult(context.Background(), &domain.SCEPProbeResult{
		ID:        "probe-no-repo",
		TargetURL: "https://example.com/scep",
	})
}

// TestPersistProbeResult_RepoErrorDoesNotFailCaller pins the
// "best-effort persist" contract documented on persistProbeResult: a
// repo write failure MUST NOT bubble back to the probe caller (the
// probe's primary contract is "run + return," not "run + persist").
// The repo's insertCalls counter MUST still be bumped so an operator
// can prove the persist code path was reached even when it failed.
func TestPersistProbeResult_RepoErrorDoesNotFailCaller(t *testing.T) {
	repo := &stubSCEPProbeRepo{insertErr: errors.New("simulated db down")}
	s := newScepProbeServiceForTest(t)
	s.SetSCEPProbeRepo(repo)

	s.persistProbeResult(context.Background(), &domain.SCEPProbeResult{
		ID:        "probe-err",
		TargetURL: "https://example.com/scep",
	})
	if repo.insertCalls != 1 {
		t.Errorf("Insert calls = %d, want 1", repo.insertCalls)
	}

	// A logger-less service MUST also survive a repo error — the warn-
	// log branch guards on `s.logger != nil`. Walk the same code path
	// with a logger-nil service to exercise that defensive guard.
	sNoLog := &NetworkScanService{nowFn: time.Now}
	sNoLog.SetSCEPProbeRepo(repo)
	sNoLog.persistProbeResult(context.Background(), &domain.SCEPProbeResult{
		ID:        "probe-err-nologger",
		TargetURL: "https://example.com/scep",
	})
	if repo.insertCalls != 2 {
		t.Errorf("Insert calls (after nologger run) = %d, want 2", repo.insertCalls)
	}
}

// TestListRecentSCEPProbes_NilRepoReturnsEmptySlice pins the
// "JSON-clean empty" contract documented on ListRecentSCEPProbes —
// the absence of a repo MUST surface as an empty slice (not nil) so
// the GUI's JSON consumer doesn't render `null` instead of `[]`.
// Critical for the React Network Scan page that .map()s over the
// result and would crash on null.
func TestListRecentSCEPProbes_NilRepoReturnsEmptySlice(t *testing.T) {
	s := newScepProbeServiceForTest(t)
	got, err := s.ListRecentSCEPProbes(context.Background(), 50)
	if err != nil {
		t.Fatalf("ListRecentSCEPProbes (nil repo): %v", err)
	}
	if got == nil {
		t.Fatal("ListRecentSCEPProbes (nil repo) returned nil, want empty slice for JSON cleanliness")
	}
	if len(got) != 0 {
		t.Errorf("ListRecentSCEPProbes (nil repo) = %d items, want 0", len(got))
	}
}

// TestListRecentSCEPProbes_DelegatesToRepo verifies the wired-repo
// path: the limit value flows through to the repository unmodified
// (the [1, 200] clamp lives at the handler layer, not the service —
// this test pins the service is a thin pass-through).
func TestListRecentSCEPProbes_DelegatesToRepo(t *testing.T) {
	repo := &stubSCEPProbeRepo{
		listResults: []*domain.SCEPProbeResult{
			{ID: "probe-1", TargetURL: "https://a.example.com/scep"},
			{ID: "probe-2", TargetURL: "https://b.example.com/scep"},
		},
	}
	s := newScepProbeServiceForTest(t)
	s.SetSCEPProbeRepo(repo)

	got, err := s.ListRecentSCEPProbes(context.Background(), 17)
	if err != nil {
		t.Fatalf("ListRecentSCEPProbes: %v", err)
	}
	if repo.listLimit != 17 {
		t.Errorf("repo.ListRecent received limit=%d, want 17", repo.listLimit)
	}
	if len(got) != 2 {
		t.Errorf("ListRecentSCEPProbes returned %d items, want 2", len(got))
	}
}

// TestDescribeCertAlgorithm covers every documented branch of the
// describe helper — including the QF1008 nil-curve defensive guard
// added in commit 9fcea95. Walking each branch keeps the staticcheck
// fix exercised in CI so a future "simplify" never reverts the nil
// check + crashes on a malformed cert.
func TestDescribeCertAlgorithm(t *testing.T) {
	rsaCert, _ := fixtureRSACertForDescribeTest(t)
	if got, want := describeCertAlgorithm(rsaCert), "RSA-2048"; got != want {
		t.Errorf("RSA describe = %q, want %q", got, want)
	}

	ecCert, _ := fixtureCACert(t, "ec-describe", time.Now().Add(-1*time.Hour), time.Now().Add(24*time.Hour))
	if got, want := describeCertAlgorithm(ecCert), "ECDSA-P-256"; got != want {
		t.Errorf("ECDSA describe = %q, want %q", got, want)
	}

	// Defensive branch: an ECDSA public key with a nil Curve. The
	// QF1008 fix keeps the explicit nil check so this case returns
	// "ECDSA" without panicking.
	bogusEC := &x509.Certificate{
		PublicKey:          &ecdsa.PublicKey{Curve: nil},
		PublicKeyAlgorithm: x509.ECDSA,
	}
	if got, want := describeCertAlgorithm(bogusEC), "ECDSA"; got != want {
		t.Errorf("nil-curve ECDSA describe = %q, want %q (QF1008 defensive branch)", got, want)
	}

	// Algorithm-only fall-through (no key type match) → Ed25519/DSA.
	ed := &x509.Certificate{PublicKeyAlgorithm: x509.Ed25519}
	if got, want := describeCertAlgorithm(ed), "Ed25519"; got != want {
		t.Errorf("Ed25519 describe = %q, want %q", got, want)
	}
	dsa := &x509.Certificate{PublicKeyAlgorithm: x509.DSA}
	if got, want := describeCertAlgorithm(dsa), "DSA"; got != want {
		t.Errorf("DSA describe = %q, want %q", got, want)
	}

	// Unrecognized → empty string (the GUI then renders "—").
	unknown := &x509.Certificate{}
	if got := describeCertAlgorithm(unknown); got != "" {
		t.Errorf("unknown describe = %q, want empty", got)
	}
}

// fixtureRSACertForDescribeTest is a tiny helper exclusive to the
// describe-algo coverage test. The package's other RSA cert helpers
// live behind type-specialized fixtures; we want a generic 2048-bit
// RSA cert + nothing else.
func fixtureRSACertForDescribeTest(t *testing.T) (*x509.Certificate, *rsa.PrivateKey) {
	t.Helper()
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("rsa.GenerateKey: %v", err)
	}
	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: "rsa-describe"},
		NotBefore:    time.Now().Add(-1 * time.Hour),
		NotAfter:     time.Now().Add(24 * time.Hour),
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		t.Fatalf("CreateCertificate: %v", err)
	}
	parsed, err := x509.ParseCertificate(der)
	if err != nil {
		t.Fatalf("ParseCertificate: %v", err)
	}
	return parsed, key
}
