package services

import (
	"context"
	"os"
	"regexp"
	"testing"

	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"

	"mgit.msbls.de/m/paliad/internal/db"
)

// shapeRegex is the lowercase dot-separated form ratified by t-paliad-204
// and enforced at the DB layer by mig 096's paliad_proceeding_code_shape
// CHECK constraint. Every active fristenrechner-category row must match.
var shapeRegex = regexp.MustCompile(`^[a-z]+\.[a-z]+\.[a-z]+$`)

// TestProceedingCodeShape walks every active fristenrechner-category row
// in paliad.proceeding_types and asserts the `code` matches the
// taxonomy regex. Catches future inserts that slip past the CHECK
// constraint (e.g. via a manual psql edit on a staging snapshot) and
// catches drift between this Go layer's stable code constants and the
// DB.
//
// Mirrors the assertions in mig 096 §8 — same regex, same shape — so a
// failure here pinpoints which row went off-shape without making a DB
// trip first.
//
// Skipped when TEST_DATABASE_URL is unset, mirroring the pattern in
// project_service_test.go.
func TestProceedingCodeShape(t *testing.T) {
	url := os.Getenv("TEST_DATABASE_URL")
	if url == "" {
		t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
	}
	if err := db.ApplyMigrations(url); err != nil {
		t.Fatalf("apply migrations: %v", err)
	}
	pool, err := sqlx.Connect("postgres", url)
	if err != nil {
		t.Fatalf("connect: %v", err)
	}
	defer pool.Close()

	ctx := context.Background()

	var rows []struct {
		ID   int    `db:"id"`
		Code string `db:"code"`
	}
	if err := pool.SelectContext(ctx, &rows,
		`SELECT id, code FROM paliad.proceeding_types
		  WHERE category = 'fristenrechner' AND is_active = true
		  ORDER BY id`); err != nil {
		t.Fatalf("load active fristenrechner rows: %v", err)
	}
	if len(rows) == 0 {
		t.Fatal("no active fristenrechner rows — mig 096 likely not applied")
	}
	for _, r := range rows {
		if !shapeRegex.MatchString(r.Code) {
			t.Errorf("proceeding_types[id=%d] code=%q does not match taxonomy shape %s",
				r.ID, r.Code, shapeRegex.String())
		}
	}

	// Spot-check the stable code constants in proceeding_mapping.go all
	// resolve to live rows. Catches a constant being renamed without a
	// matching mig update.
	stable := []string{
		CodeUPCInfringement, CodeUPCRevocation, CodeUPCCounterclaim,
		CodeUPCPreliminary, CodeUPCDamages, CodeUPCDiscovery,
		CodeUPCAppealMerits, CodeUPCAppealOrder, CodeUPCAppealCost,
		CodeDEInfringementLG, CodeDEInfringementOLG, CodeDEInfringementBGH,
		CodeDENullityBPatG, CodeDENullityBGH,
		CodeEPAGrant, CodeEPAOpposition, CodeEPAOppositionAppeal,
		CodeDPMAOpposition, CodeDPMAAppealBPatG, CodeDPMAAppealBGH,
	}
	for _, c := range stable {
		var hit int
		if err := pool.GetContext(ctx, &hit,
			`SELECT count(*) FROM paliad.proceeding_types
			  WHERE code = $1 AND is_active = true`, c); err != nil {
			t.Fatalf("count rows for %s: %v", c, err)
		}
		if hit != 1 {
			t.Errorf("stable code constant %q matches %d active rows, want 1", c, hit)
		}
	}
}

// TestProceedingCodeShapeRegexStandalone exercises the regex without
// hitting the DB so the shape rule is verified on every `go test ./...`
// run (no skip when TEST_DATABASE_URL is unset).
func TestProceedingCodeShapeRegexStandalone(t *testing.T) {
	good := []string{
		"upc.inf.cfi", "upc.rev.cfi", "upc.ccr.cfi", "upc.apl.merits",
		"upc.apl.order", "upc.apl.cost", "de.inf.lg", "de.null.bgh",
		"epa.opp.opd", "epa.grant.exa", "dpma.opp.dpma",
	}
	for _, code := range good {
		if !shapeRegex.MatchString(code) {
			t.Errorf("good code %q rejected by shape regex", code)
		}
	}
	bad := []string{
		"UPC_INF",            // old uppercase
		"upc.inf",            // missing third position
		"upc.inf.cfi.extra",  // four positions
		"upc..cfi",           // empty middle
		"upc-inf-cfi",        // dashes
		"_archived_litigation",
	}
	for _, code := range bad {
		if shapeRegex.MatchString(code) {
			t.Errorf("bad code %q accepted by shape regex", code)
		}
	}
}
