package main

import (
	"os"
	"regexp"
	"strings"
	"testing"

	"hamlog/internal/qso"
)

// Every field the bulk-edit dialog OFFERS must be one the repository accepts.
//
// These two lists live in different packages — a TypeScript field table on one
// side, a Go whitelist on the other — and they drifted apart the day the
// confirmation dates were added: the new columns went into the QSL Manager's
// filter whitelist instead of the bulk-edit one, so the dialog listed fields it
// could not write and the operator got "field … is not bulk-editable" only after
// selecting 54 QSOs and clicking Apply. Nothing in the build caught it.
//
// The test reads the ACTUAL field ids out of the .tsx rather than a copy, so a
// field added to the UI alone fails here immediately.
func TestBulkEditFieldsAreWritable(t *testing.T) {
	src, err := os.ReadFile("frontend/src/components/BulkEditModal.tsx")
	if err != nil {
		t.Skipf("frontend source not available: %v", err)
	}
	ids := regexp.MustCompile(`\{ id: '([a-z0-9_]+)'`).FindAllStringSubmatch(string(src), -1)
	if len(ids) < 10 {
		t.Fatalf("only %d field ids found — the parse is wrong, not the data", len(ids))
	}
	for _, m := range ids {
		id := m[1]
		// Handled before the column map: freq takes a numeric path (freq_hz +
		// band together) and the extras live in extras_json.
		// The integer My-station fields take their own numeric path
		// (BulkSetIntField + bulkEditableIntCols in the repo), added after this
		// guard was written — which is exactly the drift it exists to catch.
		if id == "my_dxcc" || id == "my_cq_zone" || id == "my_itu_zone" {
			continue
		}
		if id == "freq" || qso.IsBulkEditableExtra(id) {
			continue
		}
		col, ok := bulkFieldColumns[id]
		if !ok {
			t.Errorf("bulk-edit UI offers %q, which maps to no column (bulkFieldColumns)", id)
			continue
		}
		// Extras live outside the qso table (owner_callsign and friends): they are
		// written into extras_json, not a column, so the column whitelist does not
		// apply to them.
		if strings.HasPrefix(col, "extras.") {
			continue
		}
		if !qso.IsBulkEditable(col) && !qso.IsBulkEditableExtra(col) {
			t.Errorf("bulk-edit UI offers %q → column %q, which the repository refuses", id, col)
		}
	}
}

// Same contract for the filter builder: every field it offers must be one the
// query layer will accept.
func TestFilterFieldsAreQueryable(t *testing.T) {
	src, err := os.ReadFile("frontend/src/components/FilterBuilder.tsx")
	if err != nil {
		t.Skipf("frontend source not available: %v", err)
	}
	// Anchored on `type:` — the FIELDS entries carry one, the OPERATOR list does
	// not, and without it every operator (eq, contains, …) was read as a field.
	ids := regexp.MustCompile(`\{ value: '([a-z0-9_]+)', label: '[^']*', type:`).FindAllStringSubmatch(string(src), -1)
	if len(ids) < 10 {
		t.Fatalf("only %d field ids found — the parse is wrong, not the data", len(ids))
	}
	for _, m := range ids {
		col := m[1]
		if !qso.IsFilterable(col) && !qso.IsFilterableExtra(col) {
			t.Errorf("filter builder offers %q, which the query layer refuses", col)
		}
	}
}
