package db_test

import (
	"context"
	"database/sql"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"testing/fstest"
	"time"

	"github.com/owncord/server/db"
)

// openMemory opens an in-memory database for testing.
func openMemory(t *testing.T) *db.DB {
	t.Helper()
	database, err := db.Open(":memory:")
	if err != nil {
		t.Fatalf("Open(':memory:') error: %v", err)
	}
	t.Cleanup(func() { _ = database.Close() })
	return database
}

func TestOpenInMemory(t *testing.T) {
	database := openMemory(t)
	if database == nil {
		t.Fatal("Open returned nil DB")
	}
}

func TestOpenCreatesFile(t *testing.T) {
	tmpDir := t.TempDir()
	dbPath := filepath.Join(tmpDir, "test.db")

	database, err := db.Open(dbPath)
	if err != nil {
		t.Fatalf("Open() error: %v", err)
	}
	defer database.Close() //nolint:errcheck

	if _, statErr := os.Stat(dbPath); os.IsNotExist(statErr) {
		t.Error("Open() did not create the database file")
	}
}

func TestOpenInvalidPath(t *testing.T) {
	// A path to a non-existent directory should return an error.
	_, err := db.Open("/nonexistent/dir/that/does/not/exist/test.db")
	if err == nil {
		t.Error("Open() with invalid path should return error, got nil")
	}
}

func TestWALModeEnabled(t *testing.T) {
	database := openMemory(t)

	var journalMode string
	err := database.QueryRowContext(context.Background(), "PRAGMA journal_mode;").Scan(&journalMode)
	if err != nil {
		t.Fatalf("PRAGMA journal_mode query error: %v", err)
	}
	// In-memory databases return "memory" even when WAL is requested,
	// because WAL is not supported for in-memory DBs. File DBs return "wal".
	// Accept both for in-memory test; the file-based test verifies WAL properly.
	if journalMode != "memory" && journalMode != "wal" {
		t.Errorf("journal_mode = %q, want 'wal' or 'memory'", journalMode)
	}
}

func TestWALModeEnabledOnFile(t *testing.T) {
	tmpDir := t.TempDir()
	dbPath := filepath.Join(tmpDir, "wal_test.db")

	database, err := db.Open(dbPath)
	if err != nil {
		t.Fatalf("Open() error: %v", err)
	}
	defer database.Close() //nolint:errcheck

	var journalMode string
	if err := database.QueryRowContext(context.Background(), "PRAGMA journal_mode;").Scan(&journalMode); err != nil {
		t.Fatalf("PRAGMA journal_mode query error: %v", err)
	}
	if journalMode != "wal" {
		t.Errorf("journal_mode = %q, want 'wal'", journalMode)
	}
}

func TestForeignKeysEnabled(t *testing.T) {
	database := openMemory(t)

	var fkEnabled int
	if err := database.QueryRowContext(context.Background(), "PRAGMA foreign_keys;").Scan(&fkEnabled); err != nil {
		t.Fatalf("PRAGMA foreign_keys query error: %v", err)
	}
	if fkEnabled != 1 {
		t.Errorf("foreign_keys = %d, want 1 (enabled)", fkEnabled)
	}
}

func TestMigrateCreatesAllTables(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	expectedTables := []string{
		"users", "sessions", "roles", "channels", "channel_overrides",
		"messages", "attachments", "reactions", "invites", "read_states",
		"audit_log", "login_attempts", "settings", "emoji", "sounds",
	}

	for _, table := range expectedTables {
		t.Run(table, func(t *testing.T) {
			var name string
			err := database.QueryRowContext(context.Background(),
				"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
				table,
			).Scan(&name)
			if err == sql.ErrNoRows {
				t.Errorf("table %q not found after migration", table)
			} else if err != nil {
				t.Errorf("query error for table %q: %v", table, err)
			}
		})
	}
}

func TestMigrateCreatesFTSTable(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	var name string
	err := database.QueryRowContext(context.Background(),
		"SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'",
	).Scan(&name)
	if err == sql.ErrNoRows {
		t.Error("messages_fts virtual table not found after migration")
	} else if err != nil {
		t.Errorf("query error: %v", err)
	}
}

func TestMigrateIsIdempotent(t *testing.T) {
	database := openMemory(t)

	// Run migration twice — should not error.
	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() first run error: %v", err)
	}
	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() second run error: %v", err)
	}
}

func TestMigrateInsertsDefaultRoles(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	var count int
	if err := database.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM roles").Scan(&count); err != nil {
		t.Fatalf("COUNT roles error: %v", err)
	}
	if count < 4 {
		t.Errorf("expected at least 4 default roles, got %d", count)
	}
}

func TestMigrateInsertsDefaultSettings(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	var value string
	err := database.QueryRowContext(context.Background(), "SELECT value FROM settings WHERE key='registration_open'").Scan(&value)
	if err != nil {
		t.Fatalf("settings query error: %v", err)
	}
	if value != "0" {
		t.Errorf("registration_open = %q, want '0'", value)
	}
}

func TestMigrateCreatesIndexes(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	expectedIndexes := []string{
		"idx_messages_channel",
		"idx_audit_timestamp",
		"idx_attachments_message",
		"idx_channel_overrides_role",
		"idx_messages_pinned",
	}

	for _, idx := range expectedIndexes {
		t.Run(idx, func(t *testing.T) {
			var name string
			err := database.QueryRowContext(context.Background(),
				"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
				idx,
			).Scan(&name)
			if err == sql.ErrNoRows {
				t.Errorf("index %q not found after migration", idx)
			} else if err != nil {
				t.Errorf("query error for index %q: %v", idx, err)
			}
		})
	}

	// Migration 019 drops the duplicate of the channel_overrides UNIQUE
	// auto-index; migration 020 drops the duplicates of the sessions.token
	// and invites.code UNIQUE auto-indexes.
	droppedIndexes := []string{
		"idx_channel_overrides_channel_role",
		"idx_sessions_token",
		"idx_invites_code",
	}
	for _, idx := range droppedIndexes {
		t.Run(idx+" dropped", func(t *testing.T) {
			var name string
			err := database.QueryRowContext(context.Background(),
				"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
				idx,
			).Scan(&name)
			if err == nil {
				t.Errorf("%s still exists after migrations", idx)
			} else if err != sql.ErrNoRows {
				t.Errorf("query error: %v", err)
			}
		})
	}
}

func TestMigrateScopesFTSUpdateTriggerToContent(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	// Migration 019 recreates messages_au as AFTER UPDATE OF content, so
	// pin/soft-delete updates stop paying for a full FTS reindex. The trigger
	// must exist and be scoped to content updates.
	var sqlText string
	err := database.QueryRowContext(context.Background(),
		"SELECT sql FROM sqlite_master WHERE type='trigger' AND name='messages_au'",
	).Scan(&sqlText)
	if err != nil {
		t.Fatalf("messages_au trigger not found after migration: %v", err)
	}
	upper := strings.ToUpper(sqlText)
	if !strings.Contains(upper, "AFTER UPDATE OF CONTENT") {
		t.Errorf("messages_au = %q, want AFTER UPDATE OF content scope", sqlText)
	}
}

func TestCloseIdempotent(t *testing.T) {
	database, err := db.Open(":memory:")
	if err != nil {
		t.Fatalf("Open error: %v", err)
	}

	if err := database.Close(); err != nil {
		t.Errorf("Close() first call error: %v", err)
	}
}

func TestQueryRow(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	// Verify we can run a simple query via the exposed DB.
	var schemaVersion string
	err := database.QueryRowContext(context.Background(), "SELECT value FROM settings WHERE key='schema_version'").Scan(&schemaVersion)
	if err != nil {
		t.Fatalf("QueryRow error: %v", err)
	}
	if schemaVersion == "" {
		t.Error("schema_version should not be empty")
	}
}

func TestExec(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	// Insert a settings row using Exec.
	_, err := database.ExecContext(context.Background(), "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", "test_key", "test_val")
	if err != nil {
		t.Fatalf("Exec() error: %v", err)
	}

	var val string
	if err := database.QueryRowContext(context.Background(), "SELECT value FROM settings WHERE key='test_key'").Scan(&val); err != nil {
		t.Fatalf("QueryRow after Exec error: %v", err)
	}
	if val != "test_val" {
		t.Errorf("value = %q, want 'test_val'", val)
	}
}

func TestQuery(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	rows, err := database.QueryContext(context.Background(), "SELECT key FROM settings")
	if err != nil {
		t.Fatalf("Query() error: %v", err)
	}
	defer rows.Close() //nolint:errcheck

	var count int
	for rows.Next() {
		count++
		var key string
		if err := rows.Scan(&key); err != nil {
			t.Fatalf("rows.Scan error: %v", err)
		}
	}
	if count == 0 {
		t.Error("Query() returned no rows from settings table")
	}
}

func TestBegin(t *testing.T) {
	database := openMemory(t)

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	tx, err := database.BeginTx(context.Background(), nil)
	if err != nil {
		t.Fatalf("Begin() error: %v", err)
	}

	_, err = tx.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('tx_key', 'tx_val')")
	if err != nil {
		_ = tx.Rollback()
		t.Fatalf("tx.Exec error: %v", err)
	}

	if err := tx.Rollback(); err != nil {
		t.Fatalf("tx.Rollback error: %v", err)
	}

	// After rollback, tx_key should not exist.
	var val string
	err = database.QueryRowContext(context.Background(), "SELECT value FROM settings WHERE key='tx_key'").Scan(&val)
	if err == nil {
		t.Error("tx_key should not exist after rollback")
	}
}

func TestSQLDb(t *testing.T) {
	database := openMemory(t)
	sqlDB := database.SQLDb()
	if sqlDB == nil {
		t.Error("SQLDb() returned nil")
	}
}

// failReadFS implements fs.FS with a ReadDir that returns a file but
// ReadFile always errors — used to test the read-file error path in MigrateFS.
type failReadFS struct{}

func (failReadFS) Open(name string) (fs.File, error) {
	if name == "." {
		return &fakeDir{}, nil
	}
	return nil, fmt.Errorf("read error for %s", name)
}

type fakeDir struct{ pos int }

func (d *fakeDir) Read([]byte) (int, error) { return 0, io.EOF }
func (d *fakeDir) Close() error             { return nil }
func (d *fakeDir) Stat() (fs.FileInfo, error) {
	return fakeDirInfo{}, nil
}

func (d *fakeDir) ReadDir(n int) ([]fs.DirEntry, error) {
	if d.pos > 0 {
		return nil, io.EOF
	}
	d.pos++
	return []fs.DirEntry{fakeDirEntry{}}, nil
}

type fakeDirInfo struct{}

func (fakeDirInfo) Name() string       { return "." }
func (fakeDirInfo) Size() int64        { return 0 }
func (fakeDirInfo) Mode() fs.FileMode  { return fs.ModeDir | 0o755 }
func (fakeDirInfo) ModTime() time.Time { return time.Time{} }
func (fakeDirInfo) IsDir() bool        { return true }
func (fakeDirInfo) Sys() any           { return nil }

type fakeDirEntry struct{}

func (fakeDirEntry) Name() string               { return "001_fail.sql" }
func (fakeDirEntry) IsDir() bool                { return false }
func (fakeDirEntry) Type() fs.FileMode          { return 0 }
func (fakeDirEntry) Info() (fs.FileInfo, error) { return fakeFileInfo{}, nil }

type fakeFileInfo struct{}

func (fakeFileInfo) Name() string       { return "001_fail.sql" }
func (fakeFileInfo) Size() int64        { return 0 }
func (fakeFileInfo) Mode() fs.FileMode  { return 0o644 }
func (fakeFileInfo) ModTime() time.Time { return time.Time{} }
func (fakeFileInfo) IsDir() bool        { return false }
func (fakeFileInfo) Sys() any           { return nil }

func TestMigrateFSReadFileError(t *testing.T) {
	database := openMemory(t)

	err := db.MigrateFS(database, failReadFS{})
	if err == nil {
		t.Error("MigrateFS() should return error when ReadFile fails")
	}
}

func TestMigrateFSInvalidSQL(t *testing.T) {
	database := openMemory(t)

	// Create an in-memory FS with invalid SQL to trigger an exec error.
	badFS := fstest.MapFS{
		"001_bad.sql": &fstest.MapFile{
			Data: []byte("THIS IS NOT VALID SQL !!!@@@###"),
		},
	}

	err := db.MigrateFS(database, badFS)
	if err == nil {
		t.Error("MigrateFS() should return error for invalid SQL, got nil")
	}
}

func TestMigrateFSSkipsNonSQL(t *testing.T) {
	database := openMemory(t)

	// FS with non-.sql files should be skipped without error.
	mixedFS := fstest.MapFS{
		"README.md": &fstest.MapFile{Data: []byte("not sql")},
		"001_ok.sql": &fstest.MapFile{
			Data: []byte("CREATE TABLE IF NOT EXISTS test_skip (id INTEGER PRIMARY KEY);"),
		},
	}

	if err := db.MigrateFS(database, mixedFS); err != nil {
		t.Fatalf("MigrateFS() error: %v", err)
	}

	// The table from the .sql file should exist.
	var name string
	if err := database.QueryRowContext(context.Background(),
		"SELECT name FROM sqlite_master WHERE type='table' AND name='test_skip'",
	).Scan(&name); err != nil {
		t.Error("table test_skip not found after MigrateFS")
	}
}

func TestOpenPingFails(t *testing.T) {
	// Providing a path in a non-existent directory should fail.
	_, err := db.Open("/no/such/directory/db.sqlite")
	if err == nil {
		t.Error("Open() should fail for inaccessible path")
	}
}

func TestMigrateWALAndFKOnFile(t *testing.T) {
	// Verify Open sets WAL and foreign_keys on a file-backed DB, then migrate.
	tmpDir := t.TempDir()
	dbPath := filepath.Join(tmpDir, "migrate_test.db")

	database, err := db.Open(dbPath)
	if err != nil {
		t.Fatalf("Open() error: %v", err)
	}
	defer database.Close() //nolint:errcheck

	if err := db.Migrate(database); err != nil {
		t.Fatalf("Migrate() error: %v", err)
	}

	// Tables should exist.
	var name string
	if err := database.QueryRowContext(context.Background(),
		"SELECT name FROM sqlite_master WHERE type='table' AND name='users'",
	).Scan(&name); err != nil {
		t.Errorf("users table not found after migration on file db: %v", err)
	}
}
