package mysqlbackend

import (
	"context"
	"sync"
	"testing"
	"time"

	"go.uber.org/zap"
)

type fakeStmt struct {
	mu     sync.Mutex
	closed bool
}

func (f *fakeStmt) Close() error {
	f.mu.Lock()
	f.closed = true
	f.mu.Unlock()
	return nil
}

func (f *fakeStmt) isClosed() bool {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.closed
}

type fakePreparer struct {
	mu    sync.Mutex
	calls map[string]int
	stmts map[string]*fakeStmt
}

func newFakePreparer() *fakePreparer {
	return &fakePreparer{
		calls: make(map[string]int),
		stmts: make(map[string]*fakeStmt),
	}
}

func (p *fakePreparer) PrepareContext(_ context.Context, query string) (stmtHandle, error) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.calls[query]++
	stmt := &fakeStmt{}
	p.stmts[query] = stmt
	return stmt, nil
}

func (p *fakePreparer) callCount(query string) int {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.calls[query]
}

func (p *fakePreparer) stmt(query string) *fakeStmt {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.stmts[query]
}

func TestStmtCacheLRUEviction(t *testing.T) {
	cache := newStmtCache(PreparedStatementsConfig{
		Enabled:       true,
		MaxStatements: 2,
	}, zap.NewNop())
	if cache == nil {
		t.Fatalf("expected cache initialized")
	}
	prep := newFakePreparer()
	ctx := context.Background()

	if _, err := cache.prepare(ctx, prep, "stmt_a"); err != nil {
		t.Fatalf("prepare stmt_a failed: %v", err)
	}
	if _, err := cache.prepare(ctx, prep, "stmt_b"); err != nil {
		t.Fatalf("prepare stmt_b failed: %v", err)
	}
	if _, err := cache.prepare(ctx, prep, "stmt_c"); err != nil {
		t.Fatalf("prepare stmt_c failed: %v", err)
	}

	if cache.stats().Size != 2 {
		t.Fatalf("expected cache size 2, got %d", cache.stats().Size)
	}
	stmtA := prep.stmt("stmt_a")
	if stmtA == nil || !stmtA.isClosed() {
		t.Fatalf("expected stmt_a evicted and closed")
	}
}

func TestStmtCacheTTLExpiry(t *testing.T) {
	cache := newStmtCache(PreparedStatementsConfig{
		Enabled:       true,
		MaxStatements: 2,
		IdleTTL:       time.Minute,
	}, zap.NewNop())
	prep := newFakePreparer()
	ctx := context.Background()

	if _, err := cache.prepare(ctx, prep, "stmt_x"); err != nil {
		t.Fatalf("prepare stmt_x failed: %v", err)
	}
	firstStmt := prep.stmt("stmt_x")

	cache.mu.Lock()
	if el := cache.entries["stmt_x"]; el != nil {
		entry := el.Value.(*stmtEntry)
		entry.lastUse = time.Now().Add(-2 * time.Minute)
	}
	cache.mu.Unlock()

	if _, err := cache.prepare(ctx, prep, "stmt_x"); err != nil {
		t.Fatalf("re-prepare stmt_x failed: %v", err)
	}

	if got := prep.callCount("stmt_x"); got != 2 {
		t.Fatalf("expected stmt_x to be prepared twice, got %d", got)
	}
	stmtX := prep.stmt("stmt_x")
	if stmtX == nil {
		t.Fatalf("expected stmt_x handle")
	}
	if firstStmt != nil && !firstStmt.isClosed() {
		t.Fatalf("expected first stmt_x handle to be closed after expiry")
	}
}

func TestStmtCacheCleanupExpiresOldEntries(t *testing.T) {
	cache := newStmtCache(PreparedStatementsConfig{
		Enabled:       true,
		MaxStatements: 2,
		IdleTTL:       time.Second,
	}, zap.NewNop())
	prep := newFakePreparer()
	ctx := context.Background()

	if _, err := cache.prepare(ctx, prep, "stmt_old"); err != nil {
		t.Fatalf("prepare stmt_old failed: %v", err)
	}

	cache.mu.Lock()
	if el := cache.entries["stmt_old"]; el != nil {
		entry := el.Value.(*stmtEntry)
		entry.lastUse = time.Now().Add(-2 * time.Second)
	}
	cache.mu.Unlock()

	cache.cleanup()
	if cache.stats().Size != 0 {
		t.Fatalf("expected cache empty after cleanup, got %d", cache.stats().Size)
	}
	stmtOld := prep.stmt("stmt_old")
	if stmtOld == nil || !stmtOld.isClosed() {
		t.Fatalf("expected stmt_old closed after cleanup")
	}
}
