package qso

import (
	"context"
	"database/sql"
	"encoding/json"
	"path/filepath"
	"testing"

	_ "modernc.org/sqlite"
)

// openBulkTestDB builds the minimum of the qso table this needs. It does not go
// through db.Open (that would pull the whole migration set and an import cycle);
// the columns BulkSetExtra touches are extras_json and updated_at.
func openBulkTestDB(t *testing.T) *sql.DB {
	t.Helper()
	conn, err := sql.Open("sqlite", "file:"+filepath.Join(t.TempDir(), "t.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { conn.Close() })
	if _, err := conn.Exec(`CREATE TABLE qso (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		callsign TEXT NOT NULL,
		extras_json TEXT,
		updated_at TEXT
	)`); err != nil {
		t.Fatal(err)
	}
	return conn
}

func extras(t *testing.T, conn *sql.DB, id int64) map[string]any {
	t.Helper()
	var raw sql.NullString
	if err := conn.QueryRow(`SELECT extras_json FROM qso WHERE id = ?`, id).Scan(&raw); err != nil {
		t.Fatal(err)
	}
	if !raw.Valid || raw.String == "" {
		return map[string]any{}
	}
	var m map[string]any
	if err := json.Unmarshal([]byte(raw.String), &m); err != nil {
		t.Fatalf("extras_json is not valid JSON (%q): %v", raw.String, err)
	}
	return m
}

// OWNER_CALLSIGN has no promoted column, so bulk-editing it means merging a key
// into extras_json. The thing that must not happen is collateral damage: the
// other ADIF extras on the same QSO have to survive.
func TestBulkSetExtraPreservesOtherExtras(t *testing.T) {
	conn := openBulkTestDB(t)
	r := &Repo{db: conn}
	ctx := context.Background()

	// Two QSOs with existing extras, one with none at all (NULL column).
	conn.Exec(`INSERT INTO qso (callsign, extras_json) VALUES ('F5LIT', '{"SILENT_KEY":"Y","ANT_PATH":"S"}')`)
	conn.Exec(`INSERT INTO qso (callsign, extras_json) VALUES ('PA3EYF', '{"ANT_PATH":"L"}')`)
	conn.Exec(`INSERT INTO qso (callsign, extras_json) VALUES ('F4BPO', NULL)`)

	n, err := r.BulkSetExtra(ctx, []int64{1, 2, 3}, "OWNER_CALLSIGN", "TM2Q")
	if err != nil {
		t.Fatal(err)
	}
	if n != 3 {
		t.Errorf("updated %d rows, want 3", n)
	}

	e1 := extras(t, conn, 1)
	if e1["OWNER_CALLSIGN"] != "TM2Q" {
		t.Errorf("row 1 OWNER_CALLSIGN = %v, want TM2Q", e1["OWNER_CALLSIGN"])
	}
	if e1["SILENT_KEY"] != "Y" || e1["ANT_PATH"] != "S" {
		t.Errorf("row 1 lost its other extras: %v", e1)
	}
	// A NULL extras_json must become a valid object, not stay null or hold "null".
	if e3 := extras(t, conn, 3); e3["OWNER_CALLSIGN"] != "TM2Q" {
		t.Errorf("row 3 (extras_json was NULL) = %v, want OWNER_CALLSIGN=TM2Q", e3)
	}
}

// Clearing the field must REMOVE the key: a blank extra would otherwise be
// carried into every ADIF export from then on.
func TestBulkSetExtraEmptyRemovesKey(t *testing.T) {
	conn := openBulkTestDB(t)
	r := &Repo{db: conn}
	conn.Exec(`INSERT INTO qso (callsign, extras_json) VALUES ('F5LIT', '{"OWNER_CALLSIGN":"TM2Q","ANT_PATH":"S"}')`)

	if _, err := r.BulkSetExtra(context.Background(), []int64{1}, "OWNER_CALLSIGN", ""); err != nil {
		t.Fatal(err)
	}
	e := extras(t, conn, 1)
	if _, present := e["OWNER_CALLSIGN"]; present {
		t.Errorf("OWNER_CALLSIGN should be gone, got %v", e)
	}
	if e["ANT_PATH"] != "S" {
		t.Errorf("clearing one extra removed another: %v", e)
	}
}

// The frontend field id must resolve to the ADIF key, and nothing else must slip
// through — this map is the whitelist guarding a spliced JSON path.
func TestBulkExtraKeyWhitelist(t *testing.T) {
	if got := BulkExtraKey("owner_callsign"); got != "OWNER_CALLSIGN" {
		t.Errorf(`BulkExtraKey("owner_callsign") = %q, want "OWNER_CALLSIGN"`, got)
	}
	for _, bad := range []string{"", "callsign", "notes", "OWNER_CALLSIGN", "owner_callsign'"} {
		if got := BulkExtraKey(bad); got != "" {
			t.Errorf("BulkExtraKey(%q) = %q, want empty", bad, got)
		}
	}
}
