package services

// Pure-function tests for the Backup Mode runtime (t-paliad-246 / m/paliad#77).
//
// Live DB behaviour (the actual org dump end-to-end) needs a Postgres;
// it would live in backup_service_live_test.go under TEST_DATABASE_URL.
// This file covers the bits that don't need a database:
//
//   - orgSheetQueries registry shape: no duplicates, no excluded
//     paliadin sheets, predictable prefix split between entity and ref.
//   - LocalDiskStore Put / Get / Delete round-trip, key validation,
//     URI traversal rejection.

import (
	"bytes"
	"context"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// ---------------------------------------------------------------------------
// orgSheetQueries registry
// ---------------------------------------------------------------------------

func TestOrgSheetQueries_NoDuplicates(t *testing.T) {
	seen := map[string]bool{}
	for _, sq := range orgSheetQueries() {
		if seen[sq.SheetName] {
			t.Fatalf("duplicate sheet name in orgSheetQueries: %q", sq.SheetName)
		}
		seen[sq.SheetName] = true
	}
}

func TestOrgSheetQueries_ExcludesPaliadinTables(t *testing.T) {
	// m's t-paliad-214 Q5 decision + this design's §11 Q3 default:
	// paliadin_turns and paliadin_aichat_conversation must be ABSENT
	// from the registry (structural exclusion, not just column-drop).
	for _, sq := range orgSheetQueries() {
		name := sq.SheetName
		if strings.Contains(name, "paliadin") {
			t.Fatalf("orgSheetQueries leaked paliadin sheet: %q (m's Q3 mandates structural exclusion)", name)
		}
		// Belt-and-braces: SQL bodies should not reference the tables
		// either (no UNION joins, no subqueries pulling them in).
		if strings.Contains(sq.SQL, "paliadin_turns") || strings.Contains(sq.SQL, "paliadin_aichat_conversation") {
			t.Fatalf("orgSheetQueries[%q] SQL references a paliadin table: %s", name, sq.SQL)
		}
	}
}

func TestOrgSheetQueries_RefSheetsPrefixed(t *testing.T) {
	// Every sheet whose data is read-only reference material is
	// expected to use the `ref__` prefix. The writer's downstream
	// consumers rely on this convention to group reference data
	// visually in the workbook.
	for _, sq := range orgSheetQueries() {
		if !strings.HasPrefix(sq.SheetName, "ref__") {
			continue
		}
		// Reference sheets shouldn't carry per-row WHERE clauses (they
		// dump the whole reference table for portability).
		if strings.Contains(strings.ToUpper(sq.SQL), "WHERE") {
			t.Fatalf("orgSheetQueries[%q] is ref__ but has a WHERE clause; reference sheets dump the whole table", sq.SheetName)
		}
	}
}

func TestOrgSheetQueries_OrderByForDeterminism(t *testing.T) {
	// Every sheet must specify an ORDER BY so the byte-deterministic
	// contract from t-paliad-214 §3 holds across runs.
	for _, sq := range orgSheetQueries() {
		if !strings.Contains(strings.ToUpper(sq.SQL), "ORDER BY") {
			t.Fatalf("orgSheetQueries[%q] missing ORDER BY (determinism contract): %s", sq.SheetName, sq.SQL)
		}
	}
}

// ---------------------------------------------------------------------------
// LocalDiskStore round-trip
// ---------------------------------------------------------------------------

func TestLocalDiskStore_RoundTrip(t *testing.T) {
	dir := t.TempDir()
	store, err := NewLocalDiskStore(dir)
	if err != nil {
		t.Fatalf("NewLocalDiskStore: %v", err)
	}
	ctx := context.Background()
	want := []byte("hello backup\n")

	uri, err := store.Put(ctx, "test.zip", want)
	if err != nil {
		t.Fatalf("Put: %v", err)
	}
	if !strings.HasPrefix(uri, "file://") {
		t.Fatalf("expected file:// uri, got %q", uri)
	}
	rc, size, err := store.Get(ctx, uri)
	if err != nil {
		t.Fatalf("Get: %v", err)
	}
	defer rc.Close()
	if size != int64(len(want)) {
		t.Fatalf("Get size = %d, want %d", size, len(want))
	}
	got, err := io.ReadAll(rc)
	if err != nil {
		t.Fatalf("ReadAll: %v", err)
	}
	if !bytes.Equal(got, want) {
		t.Fatalf("Get body = %q, want %q", got, want)
	}
	if err := store.Delete(ctx, uri); err != nil {
		t.Fatalf("Delete: %v", err)
	}
	// File should be gone; Get returns an error.
	if _, _, err := store.Get(ctx, uri); err == nil {
		t.Fatalf("Get after Delete should fail")
	}
	// Delete is idempotent.
	if err := store.Delete(ctx, uri); err != nil {
		t.Fatalf("idempotent Delete: %v", err)
	}
}

func TestLocalDiskStore_RejectsBadKeys(t *testing.T) {
	dir := t.TempDir()
	store, err := NewLocalDiskStore(dir)
	if err != nil {
		t.Fatalf("NewLocalDiskStore: %v", err)
	}
	ctx := context.Background()
	cases := []string{
		"",
		"sub/dir/file.zip",
		"..\\evil.zip",
		"../escape.zip",
		"/abs/path.zip",
	}
	for _, k := range cases {
		if _, err := store.Put(ctx, k, []byte("x")); err == nil {
			t.Fatalf("Put with bad key %q should fail", k)
		}
	}
}

func TestLocalDiskStore_RejectsURIOutsideDir(t *testing.T) {
	dir := t.TempDir()
	store, err := NewLocalDiskStore(dir)
	if err != nil {
		t.Fatalf("NewLocalDiskStore: %v", err)
	}
	ctx := context.Background()
	// A file:// URI pointing outside the store dir must be rejected
	// by both Get and Delete (defense in depth against a corrupted
	// catalog row).
	outside := "file://" + filepath.Join(filepath.Dir(dir), "elsewhere.zip")
	if _, _, err := store.Get(ctx, outside); err == nil {
		t.Fatalf("Get outside store dir should fail")
	}
	if err := store.Delete(ctx, outside); err == nil {
		t.Fatalf("Delete outside store dir should fail")
	}
	// Wrong scheme is also rejected.
	if _, _, err := store.Get(ctx, "https://example.com/foo.zip"); err == nil {
		t.Fatalf("Get with non-file:// scheme should fail")
	}
}

func TestLocalDiskStore_CreatesDir(t *testing.T) {
	// A non-existent parent gets created at construction; mode 0700.
	base := t.TempDir()
	target := filepath.Join(base, "nested", "exports")
	store, err := NewLocalDiskStore(target)
	if err != nil {
		t.Fatalf("NewLocalDiskStore(non-existent): %v", err)
	}
	info, err := os.Stat(target)
	if err != nil {
		t.Fatalf("expected store dir to exist: %v", err)
	}
	if !info.IsDir() {
		t.Fatalf("expected directory, got file")
	}
	// Smoke-write to confirm the dir is actually usable.
	if _, err := store.Put(context.Background(), "ok.zip", []byte{}); err != nil {
		t.Fatalf("Put into fresh dir: %v", err)
	}
}
