package lease

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"testing"
	"time"

	lsfile "github.com/benbjohnson/litestream/file"
	"modernc.org/sqlite"
)

func TestCheckerClosedCheckpointFinalizationProbe(t *testing.T) {
	backend := &lockStore{}
	h := localHeld(t, backend, 30*time.Second)
	rn := h.rn
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()
	path := filepath.Join(t.TempDir(), "source.db")
	seedDB(t, path)
	replica := t.TempDir()
	h.replicaClient = lsfile.NewReplicaClient(replica)
	receipt, err := h.Checkpoint(ctx, path)
	if err != nil {
		t.Fatal(err)
	}
	if receipt.CompletedAt.IsZero() || rn != h.rn {
		t.Fatal("checkpoint ownership")
	}
	for _, suffix := range []string{"-wal", "-shm"} {
		s, e := os.Stat(path + suffix)
		if e != nil {
			t.Fatalf("expected PERSIST_WAL sidecar%s: %v", suffix, e)
		}
		t.Logf("after Litestream Close%s size=%d", suffix, s.Size())
	}
	u := url.URL{Scheme: "file", Path: path}
	q := u.Query()
	q.Set("mode", "rw")
	u.RawQuery = q.Encode()
	d, err := sql.Open("sqlite", u.String())
	if err != nil {
		t.Fatal(err)
	}
	d.SetMaxOpenConns(1)
	d.SetMaxIdleConns(1)
	conn, err := d.Conn(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if err = conn.Raw(func(raw any) error {
		f, ok := raw.(sqlite.FileControl)
		if !ok {
			return fmt.Errorf("no file control")
		}
		before, e := f.FileControlPersistWAL("main", -1)
		if e != nil {
			return e
		}
		t.Logf("ordinary PERSIST_WAL before explicit clear=%d", before)
		after, e := f.FileControlPersistWAL("main", 0)
		if e != nil {
			return e
		}
		if after != 0 {
			return fmt.Errorf("persist remains%d", after)
		}
		return nil
	}); err != nil {
		t.Fatal(err)
	}
	var integrity string
	if err = conn.QueryRowContext(ctx, "PRAGMA integrity_check").Scan(&integrity); err != nil || integrity != "ok" {
		t.Fatalf("integrity=%q err=%v", integrity, err)
	}
	var busy, log, done int
	if err = conn.QueryRowContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)").Scan(&busy, &log, &done); err != nil || busy != 0 || log != 0 || done != 0 {
		t.Fatalf("checkpoint %d %d %d err=%v", busy, log, done, err)
	}
	if err = conn.Close(); err != nil {
		t.Fatal(err)
	}
	if err = d.Close(); err != nil {
		t.Fatal(err)
	}
	absent := func() {
		t.Helper()
		for _, suffix := range []string{"-wal", "-shm", "-journal"} {
			if _, e := os.Lstat(path + suffix); !os.IsNotExist(e) {
				t.Fatalf("sidecar remains%s err=%v", suffix, e)
			}
		}
	}
	absent()
	if rn != h.rn {
		t.Fatal("renewer changed")
	}
	if _, err = h.SafeUntil(); err != nil {
		t.Fatal(err)
	}
	if _, deletes := backend.counts(); deletes != 0 {
		t.Fatal("lease released")
	}
	if peer, e := acquireTTL(ctx, "test-bucket", "one.example", "test-region", 30*time.Second); e == nil {
		peer.Release(ctx)
		t.Fatal("peer acquired")
	}
	pin, err := os.Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer pin.Close()
	beforeInfo, err := pin.Stat()
	if err != nil {
		t.Fatal(err)
	}
	hash := func() [32]byte {
		t.Helper()
		if _, e := pin.Seek(0, 0); e != nil {
			t.Fatal(e)
		}
		h := sha256.New()
		if _, e := io.Copy(h, pin); e != nil {
			t.Fatal(e)
		}
		var out [32]byte
		copy(out[:], h.Sum(nil))
		return out
	}
	before := hash()
	q.Set("mode", "ro")
	q.Set("immutable", "1")
	u.RawQuery = q.Encode()
	reader, err := sql.Open("sqlite", u.String())
	if err != nil {
		t.Fatal(err)
	}
	reader.SetMaxOpenConns(1)
	var count int
	if err = reader.QueryRowContext(ctx, "SELECT count(*) FROM t").Scan(&count); err != nil || count != 50 {
		t.Fatalf("immutable rows=%d err=%v", count, err)
	}
	if err = reader.Close(); err != nil {
		t.Fatal(err)
	}
	after := hash()
	afterInfo, err := pin.Stat()
	if err != nil {
		t.Fatal(err)
	}
	if before != after || !os.SameFile(beforeInfo, afterInfo) {
		t.Fatal("immutable check changed main")
	}
	absent()
	if count := restoreCount(t, replica, filepath.Join(t.TempDir(), "restored.db")); count != 50 {
		t.Fatalf("replica rows=%d", count)
	}
	if _, err = h.SafeUntil(); err != nil {
		t.Fatal(err)
	}
	if _, deletes := backend.counts(); deletes != 0 {
		t.Fatal("prep/check released lease")
	}
	t.Logf("PASS same Held, SQLite cleanup without unlink, immutable read preserves main SHA256=%x and absent sidecars; restored rows=50", after)
}
