package store

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

func mustNow() time.Time { return time.Now().UTC().Truncate(time.Millisecond) }

// testKey — a deterministic 32-byte key for tests.
func testKey(b byte) []byte {
	k := make([]byte, 32)
	for i := range k {
		k[i] = b + byte(i)
	}
	return k
}

func TestEncryptedRoundTrip(t *testing.T) {
	ctx := context.Background()
	dir := t.TempDir()
	cipher := filepath.Join(dir, "maven.db")
	tmpfs := filepath.Join(dir, "work.db") // tmpfs stand-in for the test
	key := testKey(1)

	s, err := OpenEncrypted(ctx, cipher, tmpfs, key)
	if err != nil {
		t.Fatalf("OpenEncrypted: %v", err)
	}
	if _, err := s.SetValue(ctx, KindSelf, "water", "tap:water", map[string]int{"ml": 250}, mustNow()); err != nil {
		t.Fatalf("SetValue: %v", err)
	}
	if err := s.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}

	// Ciphertext exists, plaintext working copy is wiped.
	if _, err := os.Stat(cipher); err != nil {
		t.Fatalf("ciphertext missing after Close: %v", err)
	}
	if _, err := os.Stat(tmpfs); !os.IsNotExist(err) {
		t.Fatalf("working copy not wiped: %v", err)
	}

	// Reopen with same key, read the fact back.
	s2, err := OpenEncrypted(ctx, cipher, tmpfs, testKey(1))
	if err != nil {
		t.Fatalf("reopen: %v", err)
	}
	defer s2.Close()
	f, err := s2.LatestFact(ctx, "water")
	if err != nil {
		t.Fatalf("LatestFact: %v", err)
	}
	if f.Key != "water" || f.Source != "tap:water" {
		t.Fatalf("got %+v", f)
	}
}

func TestWrongKeyFailsClosed(t *testing.T) {
	ctx := context.Background()
	dir := t.TempDir()
	cipher := filepath.Join(dir, "maven.db")
	tmpfs := filepath.Join(dir, "work.db")

	s, err := OpenEncrypted(ctx, cipher, tmpfs, testKey(1))
	if err != nil {
		t.Fatalf("OpenEncrypted: %v", err)
	}
	if _, err := s.SetValue(ctx, KindSelf, "water", "tap:water", 1, mustNow()); err != nil {
		t.Fatalf("SetValue: %v", err)
	}
	if err := s.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}

	s2, err := OpenEncrypted(ctx, cipher, tmpfs, testKey(9)) // wrong key
	if err == nil {
		s2.Close()
		t.Fatal("expected wrong key to fail, got nil error")
	}
	// Must not have left a decrypted working copy behind.
	if _, statErr := os.Stat(tmpfs); !os.IsNotExist(statErr) {
		t.Fatalf("wrong-key open leaked a working copy: %v", statErr)
	}
}

func TestFirstRunUpgrade(t *testing.T) {
	ctx := context.Background()
	dir := t.TempDir()
	cipher := filepath.Join(dir, "maven.db")
	tmpfs := filepath.Join(dir, "work.db")

	// Seed a PLAINTEXT db at the on-disk path (legacy state).
	plain, err := Open(ctx, cipher)
	if err != nil {
		t.Fatalf("seed Open: %v", err)
	}
	if _, err := plain.SetValue(ctx, KindSelf, "water", "tap:water", 42, mustNow()); err != nil {
		t.Fatalf("seed SetValue: %v", err)
	}
	if err := plain.Close(); err != nil {
		t.Fatalf("seed Close: %v", err)
	}
	if isCiphertext(mustRead(t, cipher)) {
		t.Fatal("seed db should be plaintext")
	}

	// Open encrypted: upgrades in place.
	key := testKey(3)
	s, err := OpenEncrypted(ctx, cipher, tmpfs, key)
	if err != nil {
		t.Fatalf("OpenEncrypted upgrade: %v", err)
	}
	f, err := s.LatestFact(ctx, "water")
	if err != nil {
		t.Fatalf("LatestFact after upgrade: %v", err)
	}
	if f.Key != "water" {
		t.Fatalf("upgraded data wrong: %+v", f)
	}
	if err := s.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}

	// On-disk file is now ciphertext, not a readable sqlite db.
	if !isCiphertext(mustRead(t, cipher)) {
		t.Fatal("on-disk file still plaintext after upgrade")
	}
	if _, err := Open(ctx, cipher); err == nil {
		t.Fatal("ciphertext opened as plaintext sqlite — should fail")
	}
}

func TestNoPlaintextValueOnDisk(t *testing.T) {
	ctx := context.Background()
	dir := t.TempDir()
	cipher := filepath.Join(dir, "maven.db")
	tmpfs := filepath.Join(dir, "work.db")

	const secret = "SUPERSECRET_MARKER_VALUE_12345"
	s, err := OpenEncrypted(ctx, cipher, tmpfs, testKey(7))
	if err != nil {
		t.Fatalf("OpenEncrypted: %v", err)
	}
	if _, err := s.SetValue(ctx, KindSelf, "note", "tap:note", secret, mustNow()); err != nil {
		t.Fatalf("SetValue: %v", err)
	}
	if err := s.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}

	if bytes.Contains(mustRead(t, cipher), []byte(secret)) {
		t.Fatal("plaintext secret found in ciphertext file")
	}
}

func mustRead(t *testing.T, p string) []byte {
	t.Helper()
	b, err := os.ReadFile(p)
	if err != nil {
		t.Fatalf("read %s: %v", p, err)
	}
	return b
}
