package store

import (
	"database/sql"
	"errors"
	"os"
	"path/filepath"
	"testing"
)

// readonly_test.go pins the OpenReadOnly contract (пакет №4): no flock (status
// works during a live run), hard read-only connections, loud errors on a missing
// DB and on a schema the binary does not match.

// TestOpenReadOnlyConcurrentWithWriter is the headline: a READ-ONLY open must
// succeed while a WRITER owns the project (the exclusive flock is held), reads
// must see the writer's committed rows, and a stray write through the read-only
// store must fail loudly instead of corrupting the live writer's state.
func TestOpenReadOnlyConcurrentWithWriter(t *testing.T) {
	path := filepath.Join(t.TempDir(), "book.db")
	w, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer w.Close()
	if err := w.InsertRequestLog(RequestLog{BookID: "b1", Chapter: 1, Stage: "draft", Role: "translator", OK: true}); err != nil {
		t.Fatal(err)
	}

	// Writer still open (flock held) — the old Open here would fail with
	// "project database is in use by another tmctl process".
	ro, err := OpenReadOnly(path)
	if err != nil {
		t.Fatalf("read-only open must not need the writer's flock: %v", err)
	}
	defer ro.Close()

	rows, err := ro.RequestLogRows("b1")
	if err != nil {
		t.Fatal(err)
	}
	if len(rows) != 1 || rows[0].Stage != "draft" {
		t.Fatalf("read-only store must see the writer's committed rows, got %+v", rows)
	}

	// A stray write must be a loud SQLITE_READONLY error, never a silent success
	// (the read-only Store aliases its write pool to the query_only connections).
	if err := ro.InsertRequestLog(RequestLog{BookID: "b1", Chapter: 2, Stage: "edit"}); err == nil {
		t.Fatal("a write through the read-only store must fail")
	}

	// The writer keeps working while the reader is open (WAL concurrent access).
	if err := w.InsertRequestLog(RequestLog{BookID: "b1", Chapter: 2, Stage: "edit", Role: "editor", OK: true}); err != nil {
		t.Fatalf("writer must not be disturbed by an open reader: %v", err)
	}
	rows, err = ro.RequestLogRows("b1")
	if err != nil {
		t.Fatal(err)
	}
	if len(rows) != 2 {
		t.Fatalf("reader must see the writer's new committed row, got %d rows", len(rows))
	}
}

// TestOpenReadOnlyMissingDB: a read of a never-run project must not silently
// create an empty database — the error wraps os.ErrNotExist so the runner can
// fall back to the creating Open on first touch.
func TestOpenReadOnlyMissingDB(t *testing.T) {
	_, err := OpenReadOnly(filepath.Join(t.TempDir(), "nope.db"))
	if err == nil {
		t.Fatal("read-only open of a missing DB must fail")
	}
	if !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("error must wrap os.ErrNotExist for the first-touch fallback, got: %v", err)
	}
}

// TestOpenReadOnlySchemaMismatch: read-only open never migrates, so a database
// behind (or ahead of) this binary's schema is a loud, actionable error — not a
// query that fails later on a missing column.
func TestOpenReadOnlySchemaMismatch(t *testing.T) {
	path := filepath.Join(t.TempDir(), "book.db")
	w, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	w.Close()

	// Roll the recorded version back one step (simulates a DB from an older binary).
	db, err := sql.Open("sqlite", "file:"+path)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Exec(`DELETE FROM schema_version WHERE version = (SELECT MAX(version) FROM schema_version)`); err != nil {
		t.Fatal(err)
	}
	db.Close()

	if _, err := OpenReadOnly(path); err == nil {
		t.Fatal("read-only open of an older-schema DB must fail loudly (it never migrates)")
	}

	// Ahead of the binary: an unknown future version must fail too.
	db, err = sql.Open("sqlite", "file:"+path)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := db.Exec(`INSERT INTO schema_version (version) VALUES (?), (?)`, len(migrations), len(migrations)+7); err != nil {
		t.Fatal(err)
	}
	db.Close()
	if _, err := OpenReadOnly(path); err == nil {
		t.Fatal("read-only open of a newer-schema DB must fail loudly")
	}
}
