// Slice B.1 (t-paliad-273) — migration 136 backfill invariants.
//
// The dry-run gate (migrate_test.go: TestMigrations_DryRun) catches
// migrations that crash on apply, but it rolls back inside its own
// transaction — the post-state assertions in mig 136's PL/pgSQL block
// run, but a future refactor of those assertions might forget a check
// or introduce a silent count drift. This test layers a Go-side
// invariant check on top so the contract is restated in test code,
// outside the PL/pgSQL block, against the resulting tables.
//
// Skipped without TEST_DATABASE_URL, same pattern as
// internal/services/submission_codes_shape_test.go.

package db

import (
	"context"
	"database/sql"
	"os"
	"testing"

	_ "github.com/lib/pq"
)

// TestMigration136_BackfillInvariants applies every embedded migration
// (which lands mig 136 along the way) and then asserts the four
// invariants the B.1 design + B.0 findings nailed down:
//
//  1. procedural_events row count = (distinct submission_codes in
//     deadline_rules) + (deadline_rules with NULL submission_code).
//     Codes-bearing branch is 1:1 per the B.0 audit (no multi-row
//     codes since the _archived_litigation.* removal); the NULL
//     branch gets one synthetic procedural_event per rule.
//  2. sequencing_rules row count = deadline_rules row count (1:1).
//  3. legal_sources row count = distinct legal_source in
//     deadline_rules (NULL excluded).
//  4. every sequencing_rules row's procedural_event_id resolves to a
//     procedural_events row (NOT NULL FK already enforces this at the
//     DB level — this test catches a future relaxation of the FK).
//  5. no two synthetic codes collide (covered by the UNIQUE on
//     procedural_events.code; restated here for documentation).
//
// The test is robust against corpus size — it derives all expected
// counts from the live deadline_rules state, so a scratch DB with 0
// rules trivially passes, and a prod-shaped scratch DB exercises the
// real invariants.
func TestMigration136_BackfillInvariants(t *testing.T) {
	url := os.Getenv("TEST_DATABASE_URL")
	if url == "" {
		t.Skip("TEST_DATABASE_URL not set — skipping mig 136 invariant test")
	}
	if err := ApplyMigrations(url); err != nil {
		t.Fatalf("apply migrations: %v", err)
	}

	conn, err := sql.Open("postgres", url)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	defer conn.Close()
	ctx := context.Background()

	var (
		drTotal, drCodesDistinct, drCodesNull, drLegalDistinct int
		peTotal, srTotal, lsTotal                              int
		orphanPE, dupSynthetic                                 int
	)

	mustQ := func(label, q string, dst *int) {
		t.Helper()
		if err := conn.QueryRowContext(ctx, q).Scan(dst); err != nil {
			t.Fatalf("%s: %v", label, err)
		}
	}

	mustQ("dr_total", `SELECT COUNT(*) FROM paliad.deadline_rules`, &drTotal)
	mustQ("dr_codes_distinct",
		`SELECT COUNT(DISTINCT submission_code) FROM paliad.deadline_rules WHERE submission_code IS NOT NULL`,
		&drCodesDistinct)
	mustQ("dr_codes_null",
		`SELECT COUNT(*) FROM paliad.deadline_rules WHERE submission_code IS NULL`,
		&drCodesNull)
	mustQ("dr_legal_distinct",
		`SELECT COUNT(DISTINCT legal_source) FROM paliad.deadline_rules WHERE legal_source IS NOT NULL`,
		&drLegalDistinct)
	mustQ("pe_total", `SELECT COUNT(*) FROM paliad.procedural_events`, &peTotal)
	mustQ("sr_total", `SELECT COUNT(*) FROM paliad.sequencing_rules`, &srTotal)
	mustQ("ls_total", `SELECT COUNT(*) FROM paliad.legal_sources`, &lsTotal)

	// Invariant 1: procedural_events = distinct_codes + null_codes
	wantPE := drCodesDistinct + drCodesNull
	if peTotal != wantPE {
		t.Errorf("procedural_events count mismatch: got %d, want %d (distinct codes=%d + null-code rules=%d)",
			peTotal, wantPE, drCodesDistinct, drCodesNull)
	}

	// Invariant 2: sequencing_rules 1:1 with deadline_rules
	if srTotal != drTotal {
		t.Errorf("sequencing_rules count mismatch: got %d, want %d (1:1 with deadline_rules)",
			srTotal, drTotal)
	}

	// Invariant 3: legal_sources = distinct legal_source
	if lsTotal != drLegalDistinct {
		t.Errorf("legal_sources count mismatch: got %d, want %d (distinct legal_source)",
			lsTotal, drLegalDistinct)
	}

	// Invariant 4: every sequencing_rules.procedural_event_id resolves
	mustQ("orphan_pe", `
		SELECT COUNT(*)
		  FROM paliad.sequencing_rules sr
		  LEFT JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id
		 WHERE pe.id IS NULL`, &orphanPE)
	if orphanPE != 0 {
		t.Errorf("FK integrity violated: %d sequencing_rules row(s) have no resolving procedural_event_id", orphanPE)
	}

	// Invariant 5: no duplicate synthetic codes
	mustQ("dup_synthetic", `
		SELECT COUNT(*) FROM (
			SELECT code FROM paliad.procedural_events
			 WHERE code LIKE 'null.%'
			 GROUP BY code
			HAVING COUNT(*) > 1
		) d`, &dupSynthetic)
	if dupSynthetic != 0 {
		t.Errorf("synthetic code uniqueness violated: %d duplicate(s) under 'null.%%' prefix", dupSynthetic)
	}

	t.Logf("mig 136 invariants OK: deadline_rules=%d, procedural_events=%d (=%d+%d), "+
		"sequencing_rules=%d, legal_sources=%d (distinct legal_source=%d)",
		drTotal, peTotal, drCodesDistinct, drCodesNull, srTotal, lsTotal, drLegalDistinct)
}
