package store

import (
	"context"
	"path/filepath"
	"testing"
	"time"
)

func openCertTestDB(t *testing.T) (*CertRepo, func()) {
	t.Helper()
	path := filepath.Join(t.TempDir(), "test.db")
	db, err := Open(path)
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	return NewCertRepo(db), func() { _ = db.Close() }
}

func sampleCert(id, nodeID, serial string, createdAt time.Time) *Cert {
	return &Cert{
		ID:          id,
		Kind:        CertKindServer,
		NodeID:      nodeID,
		SerialHex:   serial,
		SubjectCN:   "cn-" + id,
		IssuerCN:    "issuer-" + id,
		NotBefore:   createdAt.Add(-time.Hour),
		NotAfter:    createdAt.Add(24 * time.Hour),
		Fingerprint: "fp-" + id,
		SourcePath:  "/path/" + id,
		CreatedAt:   createdAt,
	}
}

func TestCertRepo_InsertAndGet(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	createdAt := time.Now().UTC().Truncate(time.Second)
	want := sampleCert("cert-1", "node-1", "AA", createdAt)

	if err := repo.Insert(ctx, want); err != nil {
		t.Fatalf("insert: %v", err)
	}

	got, err := repo.Get(ctx, "cert-1")
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if got.ID != want.ID {
		t.Errorf("id = %q, want %q", got.ID, want.ID)
	}
	if got.Kind != want.Kind {
		t.Errorf("kind = %q, want %q", got.Kind, want.Kind)
	}
	if got.NodeID != want.NodeID {
		t.Errorf("node_id = %q, want %q", got.NodeID, want.NodeID)
	}
	if got.SerialHex != want.SerialHex {
		t.Errorf("serial_hex = %q, want %q", got.SerialHex, want.SerialHex)
	}
	if got.SubjectCN != want.SubjectCN {
		t.Errorf("subject_cn = %q, want %q", got.SubjectCN, want.SubjectCN)
	}
	if got.IssuerCN != want.IssuerCN {
		t.Errorf("issuer_cn = %q, want %q", got.IssuerCN, want.IssuerCN)
	}
	if !got.NotBefore.Equal(want.NotBefore) {
		t.Errorf("not_before = %v, want %v", got.NotBefore, want.NotBefore)
	}
	if !got.NotAfter.Equal(want.NotAfter) {
		t.Errorf("not_after = %v, want %v", got.NotAfter, want.NotAfter)
	}
	if got.Fingerprint != want.Fingerprint {
		t.Errorf("fingerprint = %q, want %q", got.Fingerprint, want.Fingerprint)
	}
	if got.SourcePath != want.SourcePath {
		t.Errorf("source_path = %q, want %q", got.SourcePath, want.SourcePath)
	}
	if !got.CreatedAt.Equal(want.CreatedAt) {
		t.Errorf("created_at = %v, want %v", got.CreatedAt, want.CreatedAt)
	}
}

func TestCertRepo_InsertNil(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	if err := repo.Insert(ctx, nil); err == nil {
		t.Fatal("expected error for nil cert, got nil")
	}
}

func TestCertRepo_InsertMissingID(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	c := sampleCert("", "node-1", "AA", time.Now().UTC())
	if err := repo.Insert(ctx, c); err == nil {
		t.Fatal("expected error for missing ID, got nil")
	}
}

func TestCertRepo_InsertMissingKind(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	c := sampleCert("cert-1", "node-1", "AA", time.Now().UTC())
	c.Kind = ""
	if err := repo.Insert(ctx, c); err == nil {
		t.Fatal("expected error for missing Kind, got nil")
	}
}

func TestCertRepo_InsertDuplicateSerial(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	c1 := sampleCert("cert-1", "node-1", "DUP", time.Now().UTC())
	if err := repo.Insert(ctx, c1); err != nil {
		t.Fatalf("insert c1: %v", err)
	}
	c2 := sampleCert("cert-2", "node-1", "DUP", time.Now().UTC())
	if err := repo.Insert(ctx, c2); err == nil {
		t.Fatal("expected error for duplicate serial_hex, got nil")
	}
}

func TestCertRepo_GetMissing(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	_, err := repo.Get(ctx, "nope")
	if err != ErrNotFound {
		t.Errorf("expected ErrNotFound, got %v", err)
	}
}

func TestCertRepo_List(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	base := time.Now().UTC()
	ids := []string{"old", "mid", "new"}
	for i, id := range ids {
		c := sampleCert(id, "node-1", "S"+id, base.Add(time.Duration(i)*time.Second))
		if err := repo.Insert(ctx, c); err != nil {
			t.Fatalf("insert %s: %v", id, err)
		}
	}
	got, err := repo.List(ctx)
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(got) != 3 {
		t.Fatalf("expected 3 certs, got %d", len(got))
	}
	wantOrder := []string{"new", "mid", "old"}
	for i, want := range wantOrder {
		if got[i].ID != want {
			t.Errorf("list[%d].id = %q, want %q", i, got[i].ID, want)
		}
	}
}

func TestCertRepo_ListByNode(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	base := time.Now().UTC()
	for i, id := range []string{"a1", "a2"} {
		c := sampleCert(id, "nodeA", "SA"+id, base.Add(time.Duration(i)*time.Second))
		if err := repo.Insert(ctx, c); err != nil {
			t.Fatalf("insert %s: %v", id, err)
		}
	}
	for i, id := range []string{"b1"} {
		c := sampleCert(id, "nodeB", "SB"+id, base.Add(time.Duration(i)*time.Second))
		if err := repo.Insert(ctx, c); err != nil {
			t.Fatalf("insert %s: %v", id, err)
		}
	}

	aCerts, err := repo.ListByNode(ctx, "nodeA")
	if err != nil {
		t.Fatalf("list nodeA: %v", err)
	}
	if len(aCerts) != 2 {
		t.Errorf("expected 2 nodeA certs, got %d", len(aCerts))
	}
	for _, c := range aCerts {
		if c.NodeID != "nodeA" {
			t.Errorf("unexpected node_id %q in nodeA results", c.NodeID)
		}
	}

	bCerts, err := repo.ListByNode(ctx, "nodeB")
	if err != nil {
		t.Fatalf("list nodeB: %v", err)
	}
	if len(bCerts) != 1 {
		t.Errorf("expected 1 nodeB cert, got %d", len(bCerts))
	}
}

func TestCertRepo_LatestForKind(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	base := time.Now().UTC()
	older := sampleCert("old", "node-1", "O", base)
	newer := sampleCert("new", "node-1", "N", base.Add(time.Minute))
	if err := repo.Insert(ctx, older); err != nil {
		t.Fatalf("insert old: %v", err)
	}
	if err := repo.Insert(ctx, newer); err != nil {
		t.Fatalf("insert new: %v", err)
	}

	got, err := repo.LatestForKind(ctx, "node-1", CertKindServer)
	if err != nil {
		t.Fatalf("latest: %v", err)
	}
	if got.ID != "new" {
		t.Errorf("latest.id = %q, want new", got.ID)
	}

	_, err = repo.LatestForKind(ctx, "node-empty", CertKindServer)
	if err != ErrNotFound {
		t.Errorf("expected ErrNotFound, got %v", err)
	}
}

func TestCertRepo_PruneOlderThan(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	base := time.Now().UTC()
	for i, id := range []string{"c1", "c2", "c3", "c4"} {
		c := sampleCert(id, "node-1", "S"+id, base.Add(time.Duration(i)*time.Second))
		if err := repo.Insert(ctx, c); err != nil {
			t.Fatalf("insert %s: %v", id, err)
		}
	}

	n, err := repo.PruneOlderThan(ctx, "node-1", string(CertKindServer), 3)
	if err != nil {
		t.Fatalf("prune: %v", err)
	}
	if n != 1 {
		t.Errorf("expected 1 row deleted, got %d", n)
	}
	remaining, err := repo.ListByNode(ctx, "node-1")
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(remaining) != 3 {
		t.Errorf("expected 3 remaining, got %d", len(remaining))
	}
	for _, c := range remaining {
		if c.ID == "c1" {
			t.Errorf("expected c1 pruned, but found")
		}
	}

	n2, err := repo.PruneOlderThan(ctx, "node-1", string(CertKindServer), 0)
	if err != nil {
		t.Fatalf("prune keep=0: %v", err)
	}
	if n2 != 2 {
		t.Errorf("keep=0 treated as keep=1: expected 2 deleted, got %d", n2)
	}
	remaining2, err := repo.ListByNode(ctx, "node-1")
	if err != nil {
		t.Fatalf("list after keep=0: %v", err)
	}
	if len(remaining2) != 1 {
		t.Errorf("keep=0 treated as keep=1: expected 1 remaining, got %d", len(remaining2))
	}
	if remaining2[0].ID != "c4" {
		t.Errorf("expected newest c4 retained, got %q", remaining2[0].ID)
	}
}

func TestCertRepo_Delete(t *testing.T) {
	repo, cleanup := openCertTestDB(t)
	defer cleanup()

	ctx := context.Background()
	c := sampleCert("cert-del", "node-1", "DEL", time.Now().UTC())
	if err := repo.Insert(ctx, c); err != nil {
		t.Fatalf("insert: %v", err)
	}
	if err := repo.Delete(ctx, "cert-del"); err != nil {
		t.Fatalf("delete: %v", err)
	}
	if err := repo.Delete(ctx, "cert-del"); err != ErrNotFound {
		t.Errorf("expected ErrNotFound on second delete, got %v", err)
	}
}
