// Slice B.2 dual-write tests (t-paliad-305 / m/paliad#93).
//
// Asserts the parallel projection — paliad.procedural_events +
// paliad.sequencing_rules + paliad.legal_sources — stays in lock-step
// with paliad.deadline_rules through the full RuleEditorService
// lifecycle. Skipped when TEST_DATABASE_URL is unset.

package services

import (
	"context"
	"os"
	"testing"

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

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

// TestDualWrite_RuleEditorLifecycle walks Create → UpdateDraft →
// CloneAsDraft → Publish → Archive → Restore on RuleEditorService and
// after each operation asserts that paliad.sequencing_rules has the
// 1:1 mirror, paliad.procedural_events carries the projected identity,
// and paliad.legal_sources carries the citation.
func TestDualWrite_RuleEditorLifecycle(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()
	rules := NewDeadlineRuleService(pool)
	svc := NewRuleEditorService(pool, rules)

	cleanup := func() {
		pool.ExecContext(ctx,
			`SELECT set_config('paliad.audit_reason', 'slice b.2 test cleanup', true)`)
		// Order matters: sequencing_rules → procedural_events → legal_sources
		// (FK direction). deadline_rules cleanup last because mig 079 audit
		// trigger captures the DELETE.
		pool.ExecContext(ctx, `DELETE FROM paliad.sequencing_rules WHERE id IN (
			SELECT id FROM paliad.deadline_rules WHERE name LIKE 'SLICEB2_TEST_%'
		)`)
		pool.ExecContext(ctx, `DELETE FROM paliad.procedural_events
			WHERE code LIKE 'sliceb2.%' OR code LIKE 'null.sliceb2%'`)
		pool.ExecContext(ctx, `DELETE FROM paliad.legal_sources
			WHERE citation LIKE 'SLICEB2.%'`)
		pool.ExecContext(ctx,
			`DELETE FROM paliad.deadline_rules WHERE name LIKE 'SLICEB2_TEST_%'`)
		pool.ExecContext(ctx,
			`DELETE FROM paliad.proceeding_types WHERE code = 'SLICEB2_TEST_PT'`)
	}
	cleanup()
	defer cleanup()

	var ptID int
	if err := pool.GetContext(ctx, &ptID, `
		INSERT INTO paliad.proceeding_types (code, name, name_en, category, jurisdiction, is_active)
		VALUES ('SLICEB2_TEST_PT', 'Slice B.2 Test PT', 'Slice B.2 Test PT', 'fristenrechner', 'UPC', true)
		RETURNING id`); err != nil {
		t.Fatalf("seed proceeding_type: %v", err)
	}

	subCode := "sliceb2.create"
	legalSrc := "SLICEB2.PatG.1"

	// 1. Create — assert the parallel rows land.
	created, err := svc.Create(ctx, CreateRuleInput{
		Name:             "SLICEB2_TEST_create",
		NameEN:           "SLICEB2_TEST_create_EN",
		ProceedingTypeID: &ptID,
		SubmissionCode:   &subCode,
		LegalSource:      &legalSrc,
		DurationValue:    30,
		DurationUnit:     "days",
		Priority:         "mandatory",
	}, "B.2 dual-write create test")
	if err != nil {
		t.Fatalf("Create: %v", err)
	}

	// legal_sources should now carry SLICEB2.PatG.1
	var lsCount int
	if err := pool.GetContext(ctx, &lsCount,
		`SELECT COUNT(*) FROM paliad.legal_sources WHERE citation = $1`, legalSrc); err != nil {
		t.Fatalf("query legal_sources: %v", err)
	}
	if lsCount != 1 {
		t.Errorf("legal_sources after Create: got %d, want 1 for citation %q", lsCount, legalSrc)
	}

	// procedural_events should carry the submission_code
	var peName, peLifecycle string
	if err := pool.GetContext(ctx, &peName,
		`SELECT name FROM paliad.procedural_events WHERE code = $1`, subCode); err != nil {
		t.Fatalf("query procedural_events name: %v", err)
	}
	if peName != "SLICEB2_TEST_create" {
		t.Errorf("procedural_events.name after Create: got %q, want %q", peName, "SLICEB2_TEST_create")
	}
	if err := pool.GetContext(ctx, &peLifecycle,
		`SELECT lifecycle_state FROM paliad.procedural_events WHERE code = $1`, subCode); err != nil {
		t.Fatalf("query procedural_events lifecycle: %v", err)
	}
	if peLifecycle != "draft" {
		t.Errorf("procedural_events.lifecycle_state after Create: got %q, want %q", peLifecycle, "draft")
	}

	// sequencing_rules should have id = created.id and link to PE
	var srCount, srMatchPE int
	if err := pool.GetContext(ctx, &srCount,
		`SELECT COUNT(*) FROM paliad.sequencing_rules WHERE id = $1`, created.ID); err != nil {
		t.Fatalf("query sequencing_rules count: %v", err)
	}
	if srCount != 1 {
		t.Errorf("sequencing_rules row after Create: got %d, want 1 for id %s", srCount, created.ID)
	}
	if err := pool.GetContext(ctx, &srMatchPE, `
		SELECT COUNT(*) FROM paliad.sequencing_rules sr
		  JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id
		 WHERE sr.id = $1 AND pe.code = $2`, created.ID, subCode); err != nil {
		t.Fatalf("query sr→pe join: %v", err)
	}
	if srMatchPE != 1 {
		t.Errorf("sequencing_rules.procedural_event_id after Create: got %d join hits, want 1", srMatchPE)
	}

	// 2. UpdateDraft — change name + legal_source. Assert propagation.
	newName := "SLICEB2_TEST_updated"
	newLegal := "SLICEB2.ZPO.2"
	_, err = svc.UpdateDraft(ctx, created.ID, RulePatch{
		Name:        &newName,
		LegalSource: &newLegal,
	}, "B.2 dual-write update test")
	if err != nil {
		t.Fatalf("UpdateDraft: %v", err)
	}

	var afterName string
	if err := pool.GetContext(ctx, &afterName,
		`SELECT name FROM paliad.procedural_events WHERE code = $1`, subCode); err != nil {
		t.Fatalf("query pe.name post-update: %v", err)
	}
	if afterName != newName {
		t.Errorf("procedural_events.name after UpdateDraft: got %q, want %q", afterName, newName)
	}

	// New citation must appear in legal_sources, and procedural_events.legal_source_id
	// must point at it (idempotent UPSERT — the old SLICEB2.PatG.1 row stays).
	var pePointsAtNewLegal int
	if err := pool.GetContext(ctx, &pePointsAtNewLegal, `
		SELECT COUNT(*) FROM paliad.procedural_events pe
		  JOIN paliad.legal_sources ls ON ls.id = pe.legal_source_id
		 WHERE pe.code = $1 AND ls.citation = $2`, subCode, newLegal); err != nil {
		t.Fatalf("query pe→ls join: %v", err)
	}
	if pePointsAtNewLegal != 1 {
		t.Errorf("procedural_events.legal_source_id after UpdateDraft: got %d hits, want 1", pePointsAtNewLegal)
	}

	// 3. Publish — flip to published. Assert lifecycle mirror.
	_, err = svc.Publish(ctx, created.ID, "B.2 dual-write publish test")
	if err != nil {
		t.Fatalf("Publish: %v", err)
	}
	var srLifecycle, peLifecycleAfterPub string
	if err := pool.GetContext(ctx, &srLifecycle,
		`SELECT lifecycle_state FROM paliad.sequencing_rules WHERE id = $1`, created.ID); err != nil {
		t.Fatalf("query sr.lifecycle: %v", err)
	}
	if srLifecycle != "published" {
		t.Errorf("sequencing_rules.lifecycle_state after Publish: got %q, want %q", srLifecycle, "published")
	}
	if err := pool.GetContext(ctx, &peLifecycleAfterPub,
		`SELECT lifecycle_state FROM paliad.procedural_events WHERE code = $1`, subCode); err != nil {
		t.Fatalf("query pe.lifecycle post-publish: %v", err)
	}
	if peLifecycleAfterPub != "published" {
		t.Errorf("procedural_events.lifecycle_state after Publish: got %q, want %q", peLifecycleAfterPub, "published")
	}

	// 4. Archive — flip to archived. Assert mirror.
	_, err = svc.Archive(ctx, created.ID, "B.2 dual-write archive test")
	if err != nil {
		t.Fatalf("Archive: %v", err)
	}
	var srLifecycleArchived string
	if err := pool.GetContext(ctx, &srLifecycleArchived,
		`SELECT lifecycle_state FROM paliad.sequencing_rules WHERE id = $1`, created.ID); err != nil {
		t.Fatalf("query sr.lifecycle post-archive: %v", err)
	}
	if srLifecycleArchived != "archived" {
		t.Errorf("sequencing_rules.lifecycle_state after Archive: got %q, want %q", srLifecycleArchived, "archived")
	}

	// Slice B.4 (mig 140, t-paliad-305): the legacy paliad.deadline_rules
	// table is gone and so is CheckDualWriteDrift — there's no parallel
	// side to compare against. The INSTEAD OF triggers on the view
	// guarantee parity by construction (single TX fan-out from one
	// SQL write to three target tables).
}

// TestDualWrite_SyntheticCodeForNullSubmission asserts that a rule
// created with submission_code=NULL gets a synthetic 'null.<8hex>'
// procedural_events row matching mig 136's mint expression — so a new
// draft without a code participates in the dual-write contract without
// colliding with any code-bearing rule.
func TestDualWrite_SyntheticCodeForNullSubmission(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()
	rules := NewDeadlineRuleService(pool)
	svc := NewRuleEditorService(pool, rules)

	cleanup := func() {
		pool.ExecContext(ctx, `SELECT set_config('paliad.audit_reason', 'slice b.2 null-code cleanup', true)`)
		pool.ExecContext(ctx, `DELETE FROM paliad.sequencing_rules WHERE id IN (
			SELECT id FROM paliad.deadline_rules WHERE name = 'SLICEB2_TEST_nullcode'
		)`)
		// Synthetic PE rows are keyed off the rule's uuid; delete by name reference.
		pool.ExecContext(ctx, `DELETE FROM paliad.procedural_events
			WHERE code IN (
			  SELECT 'null.' || substring(replace(id::text, '-', ''), 1, 8)
			    FROM paliad.deadline_rules WHERE name = 'SLICEB2_TEST_nullcode'
			)`)
		pool.ExecContext(ctx, `DELETE FROM paliad.deadline_rules WHERE name = 'SLICEB2_TEST_nullcode'`)
		pool.ExecContext(ctx, `DELETE FROM paliad.proceeding_types WHERE code = 'SLICEB2_NC_PT'`)
	}
	cleanup()
	defer cleanup()

	var ptID int
	if err := pool.GetContext(ctx, &ptID, `
		INSERT INTO paliad.proceeding_types (code, name, name_en, category, jurisdiction, is_active)
		VALUES ('SLICEB2_NC_PT', 'NC PT', 'NC PT', 'fristenrechner', 'UPC', true)
		RETURNING id`); err != nil {
		t.Fatalf("seed proceeding_type: %v", err)
	}

	created, err := svc.Create(ctx, CreateRuleInput{
		Name:             "SLICEB2_TEST_nullcode",
		NameEN:           "SLICEB2_TEST_nullcode_EN",
		ProceedingTypeID: &ptID,
		// SubmissionCode intentionally NIL → tests the synthetic-code branch.
		DurationValue: 5,
		DurationUnit:  "days",
		Priority:      "mandatory",
	}, "B.2 dual-write null-code test")
	if err != nil {
		t.Fatalf("Create: %v", err)
	}

	// Compute the expected synthetic code in the same way mig 136 / the
	// dual-write helper do — keep the expression in lock-step with the
	// SQL via this Go-side mirror.
	var expectedCode string
	if err := pool.GetContext(ctx, &expectedCode,
		`SELECT 'null.' || substring(replace(id::text, '-', ''), 1, 8)
		   FROM paliad.deadline_rules WHERE id = $1`, created.ID); err != nil {
		t.Fatalf("compute expected synthetic code: %v", err)
	}

	var actualCode string
	if err := pool.GetContext(ctx, &actualCode, `
		SELECT pe.code
		  FROM paliad.procedural_events pe
		  JOIN paliad.sequencing_rules sr ON sr.procedural_event_id = pe.id
		 WHERE sr.id = $1`, created.ID); err != nil {
		t.Fatalf("query procedural_events via sequencing_rules: %v", err)
	}
	if actualCode != expectedCode {
		t.Errorf("synthetic code mismatch: got %q, want %q", actualCode, expectedCode)
	}
	if len(actualCode) != len("null.")+8 {
		t.Errorf("synthetic code length: got %d, want 13 (null.+8hex)", len(actualCode))
	}
}
