package mtls

import (
	"context"
	"crypto/x509"
	"encoding/pem"
	"testing"

	"github.com/alicebob/miniredis/v2"
	"github.com/redis/go-redis/v9"
)

// noopDB satisfies the DB interface without persisting anything.
type noopDB struct{}

func (noopDB) RecordRevocation(_ context.Context, _ string) error { return nil }

func newTestCRL(t *testing.T) (*CRL, *miniredis.Miniredis) {
	t.Helper()
	mr := miniredis.RunT(t)
	rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
	t.Cleanup(func() { rdb.Close() })
	return NewCRL(rdb, noopDB{}), mr
}

func TestCRL_RevokeAndIsRevoked(t *testing.T) {
	crl, _ := newTestCRL(t)
	ctx := context.Background()

	const nodeUUID = "node-revoked-uuid"

	if crl.IsRevoked(nodeUUID) {
		t.Fatal("IsRevoked = true before Revoke; want false")
	}

	if err := crl.Revoke(ctx, nodeUUID); err != nil {
		t.Fatalf("Revoke: %v", err)
	}

	if !crl.IsRevoked(nodeUUID) {
		t.Fatal("IsRevoked = false after Revoke; want true")
	}
}

func TestCRL_IsRevoked_UnknownNode(t *testing.T) {
	crl, _ := newTestCRL(t)

	if crl.IsRevoked("never-revoked") {
		t.Fatal("IsRevoked = true for unknown node; want false")
	}
}

func TestCRL_VerifyPeerCertificate_Revoked(t *testing.T) {
	dir := t.TempDir()
	ca := newTestCA(t, dir)

	crl, _ := newTestCRL(t)
	ctx := context.Background()

	nodeUUID := "node-revoked-cert"
	csrPEM, _ := newTestCSR(t)
	certPEM, err := ca.SignCSR(csrPEM, nodeUUID)
	if err != nil {
		t.Fatalf("SignCSR: %v", err)
	}

	block, _ := pem.Decode(certPEM)
	clientCert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		t.Fatalf("ParseCertificate: %v", err)
	}

	// Build verified chains as the TLS stack would.
	pool := x509.NewCertPool()
	pool.AddCert(ca.CACert())
	chains, err := clientCert.Verify(x509.VerifyOptions{
		Roots:     pool,
		KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
	})
	if err != nil {
		t.Fatalf("cert.Verify: %v", err)
	}

	// Before revocation: VerifyPeerCertificate must pass.
	if err := crl.VerifyPeerCertificate(nil, chains); err != nil {
		t.Fatalf("VerifyPeerCertificate before revoke: %v", err)
	}

	// Revoke the node.
	if err := crl.Revoke(ctx, nodeUUID); err != nil {
		t.Fatalf("Revoke: %v", err)
	}

	// After revocation: VerifyPeerCertificate must reject.
	if err := crl.VerifyPeerCertificate(nil, chains); err == nil {
		t.Fatal("VerifyPeerCertificate after revoke: expected error, got nil")
	}
}

func TestCRL_VerifyPeerCertificate_NoClientCert(t *testing.T) {
	crl, _ := newTestCRL(t)

	// Empty verifiedChains = no client cert presented (e.g. during Enroll).
	if err := crl.VerifyPeerCertificate(nil, nil); err != nil {
		t.Errorf("VerifyPeerCertificate with no chains: %v", err)
	}
}
