package main

import (
	"context"
	"database/sql"
	"os"
	"path/filepath"
	"sync"
	"testing"
)

// TestSQLitePragmaHook verifies that every connection opened via the registered
// "sqlite3" driver has WAL journal mode and a busy_timeout set.
func TestSQLitePragmaHook(t *testing.T) {
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "test.db")

	db, err := sql.Open("sqlite3", dbPath)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer db.Close()

	// Force a real connection to be created (Open is lazy).
	if err := db.Ping(); err != nil {
		t.Fatalf("ping: %v", err)
	}

	var journalMode string
	if err := db.QueryRow("PRAGMA journal_mode").Scan(&journalMode); err != nil {
		t.Fatalf("query journal_mode: %v", err)
	}
	if journalMode != "wal" {
		t.Errorf("journal_mode = %q, want %q", journalMode, "wal")
	}

	var busyTimeout int
	if err := db.QueryRow("PRAGMA busy_timeout").Scan(&busyTimeout); err != nil {
		t.Fatalf("query busy_timeout: %v", err)
	}
	if busyTimeout != 5000 {
		t.Errorf("busy_timeout = %d, want %d", busyTimeout, 5000)
	}
}

// TestSQLiteConcurrentWrites verifies that concurrent writers do not get
// SQLITE_BUSY errors thanks to WAL mode and busy_timeout.
func TestSQLiteConcurrentWrites(t *testing.T) {
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "concurrent.db")

	db, err := sql.Open("sqlite3", dbPath)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer db.Close()

	// Create a table to write to.
	if _, err := db.Exec(`CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT)`); err != nil {
		t.Fatalf("create table: %v", err)
	}

	// Simulate the scenario: multiple goroutines writing concurrently,
	// like mautrix crypto sync + memory store + knowledge store.
	const writers = 5
	const writesPerWriter = 50
	ctx := context.Background()

	var wg sync.WaitGroup
	errs := make(chan error, writers*writesPerWriter)

	for w := 0; w < writers; w++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for i := 0; i < writesPerWriter; i++ {
				_, err := db.ExecContext(ctx,
					`INSERT OR REPLACE INTO kv (k, v) VALUES (?, ?)`,
					// Use writer+iteration as key so they conflict
					"key", "value",
				)
				if err != nil {
					errs <- err
				}
			}
		}()
	}

	wg.Wait()
	close(errs)

	for err := range errs {
		t.Errorf("concurrent write error: %v", err)
	}
}

// TestSQLiteConcurrentWritesSeparateConnections tests with separate sql.DB
// instances (like crypto.db being opened by both mautrix and our code).
func TestSQLiteConcurrentWritesSeparateConnections(t *testing.T) {
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "shared.db")

	// Open two separate connections to the same file (simulates mautrix +
	// our memory store sharing a DB, or separate processes).
	db1, err := sql.Open("sqlite3", dbPath)
	if err != nil {
		t.Fatalf("open db1: %v", err)
	}
	defer db1.Close()

	db2, err := sql.Open("sqlite3", dbPath)
	if err != nil {
		t.Fatalf("open db2: %v", err)
	}
	defer db2.Close()

	// Create table via db1
	if _, err := db1.Exec(`CREATE TABLE t (id INTEGER PRIMARY KEY, data TEXT)`); err != nil {
		t.Fatalf("create table: %v", err)
	}

	ctx := context.Background()
	const iterations = 100

	var wg sync.WaitGroup
	errs := make(chan error, iterations*2)

	// Writer 1 (simulates mautrix SaveNextBatch)
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < iterations; i++ {
			_, err := db1.ExecContext(ctx,
				`INSERT INTO t (data) VALUES (?)`, "from_crypto_sync",
			)
			if err != nil {
				errs <- err
			}
		}
	}()

	// Writer 2 (simulates our memory store SaveMessage)
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < iterations; i++ {
			_, err := db2.ExecContext(ctx,
				`INSERT INTO t (data) VALUES (?)`, "from_memory_store",
			)
			if err != nil {
				errs <- err
			}
		}
	}()

	wg.Wait()
	close(errs)

	for err := range errs {
		t.Errorf("concurrent write error (separate conns): %v", err)
	}

	// Verify all writes succeeded
	var count int
	if err := db1.QueryRow("SELECT COUNT(*) FROM t").Scan(&count); err != nil {
		t.Fatalf("count: %v", err)
	}
	expected := iterations * 2
	if count != expected {
		t.Errorf("row count = %d, want %d", count, expected)
	}
}

// TestSQLiteWALFileCreated verifies that WAL mode actually creates the -wal file,
// confirming the pragma took effect at the filesystem level.
func TestSQLiteWALFileCreated(t *testing.T) {
	dir := t.TempDir()
	dbPath := filepath.Join(dir, "walcheck.db")

	db, err := sql.Open("sqlite3", dbPath)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer db.Close()

	// Create a table and write data to trigger WAL file creation.
	if _, err := db.Exec(`CREATE TABLE x (id INTEGER PRIMARY KEY)`); err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := db.Exec(`INSERT INTO x (id) VALUES (1)`); err != nil {
		t.Fatalf("insert: %v", err)
	}

	walPath := dbPath + "-wal"
	if _, err := os.Stat(walPath); os.IsNotExist(err) {
		t.Errorf("WAL file not created at %s — PRAGMA journal_mode=WAL may not be taking effect", walPath)
	}
}
