package packs

import (
	"strings"
	"testing"

	"github.com/pingcap-inc/nexuscrm-controlplane/layoutownership"
	"github.com/nexuscrm/shared/pkg/models"
)

// matrixCellExpectation pins, for one cell, a seed-state and post-apply
// re-classification target. The parity test below verifies that:
//
//	(1) classify(seed) == cell  (classification is stable for the given state)
//	(2) stateMatrix[cell] exists and matches the cell's intended semantics
//	(3) for repairable cells, applying transforms the observed state into
//	    stateMatrix[cell].apply.verifyAfterApply, and that target cell is OK
//
// Each repairable cell must point at an OK cell after apply (proves
// convergence). Conflict cells point at themselves (operator-only).
type matrixCellExpectation struct {
	cell   curatedLayoutCell
	seed   observedCuratedLayoutState
	intent string
}

// curatedID and objectAPIName for fixture states.
const (
	testCuratedID     = "curated_Account_default"
	testObjectAPIName = "Account"
)

func managedSeedRoute() *string {
	s := models.LayoutAssignmentRouteSourceManagedSeed
	return &s
}

func adminAuthoredRoute() *string {
	s := models.LayoutAssignmentRouteSourceAdminAuthored
	return &s
}

func opaqueRoute() *string {
	s := "legacy_unknown"
	return &s
}

// cellExpectations enumerates every cell in stateMatrix. Adding a new
// cell to the matrix without adding it here makes the parity test fail.
func cellExpectations() []matrixCellExpectation {
	return []matrixCellExpectation{
		// OK cells — verify nil. Pack-managed-row variants may refresh
		// row content (config-drift repair); AdminOwned-at-curated variants
		// additively merge pack-owned recordPanels (L2 sections/edits preserved);
		// other AdminOwned variants are route-only (L2 row content is preserved).
		{
			cell: cellOKManagedSeedAtCurated,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 1, rowExists: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: testCuratedID, TargetPresent: true,
					RouteSource: managedSeedRoute(),
				},
			},
			intent: "wildcard managed_seed at curated row, row pack-managed (steady state)",
		},
		{
			cell: cellOKManagedSeedAtCuratedAdminOwned,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 1, rowExists: true, rowAdminOwned: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: testCuratedID, TargetPresent: true,
					RouteSource:  managedSeedRoute(),
					IsCustomized: true, Source: "admin_created",
				},
			},
			intent: "managed_seed route at curated row, admin has taken over row content; coherent steady state, apply additively merges missing pack record panels without overwriting admin content",
		},
		{
			cell: cellOKAdminAuthoredAtCurated,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 1, rowExists: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: testCuratedID, TargetPresent: true,
					RouteSource: adminAuthoredRoute(),
				},
			},
			intent: "admin took over the wildcard routing to point at curated, row still pack-managed",
		},
		{
			cell: cellOKAdminAuthoredAtCuratedAdminOwned,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 1, rowExists: true, rowAdminOwned: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: testCuratedID, TargetPresent: true,
					RouteSource:  adminAuthoredRoute(),
					IsCustomized: true, Source: "admin_created",
				},
			},
			intent: "admin owns both route source and row content at curated; apply additively merges missing pack record panels while preserving L2 authority over sections/edits",
		},
		{
			cell: cellOKAdminAuthoredAtAdminOther,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 2, assignmentCount: 1, rowExists: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: "PackManagedAlternativeLayout", TargetPresent: true,
					RouteSource:  adminAuthoredRoute(),
					IsCustomized: false, Source: "curated",
				},
			},
			intent: "admin chose a different default layout via admin-authored wildcard; target and curated row both pack-managed",
		},
		{
			cell: cellOKAdminAuthoredAtAdminOtherAdminOwned,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 2, assignmentCount: 1, rowExists: true, rowAdminOwned: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: "AdminCustomLayout", TargetPresent: true,
					RouteSource:  adminAuthoredRoute(),
					IsCustomized: true, Source: "admin_created",
				},
			},
			intent: "admin chose a different default and also owns the curated fallback row; additively merge missing pack record panels and clear the curated default flag, preserving L2 row content",
		},

		// Missing-class repairable cells.
		{
			cell: cellMissingGreenfield,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 0, assignmentCount: 0, rowExists: false,
			},
			intent: "brand-new install: nothing exists, seed managed",
		},
		{
			cell: cellMissingCuratedWithSiblings,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 0, rowExists: false,
			},
			intent: "curated row deleted but other live layouts remain; upsert curated and seed managed wildcard",
		},
		{
			cell: cellMissingOrphanWildcardGreenfield,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 0, assignmentCount: 1, rowExists: false,
				// no live target → wildcardTarget remains nil
			},
			intent: "orphan wildcard with no live target, nothing else; safe to recreate",
		},
		{
			cell: cellMissingCuratedUnderAdminRoute,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 1, rowExists: false,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: "AdminCustomLayout", TargetPresent: true,
					RouteSource:  adminAuthoredRoute(),
					IsCustomized: true, Source: "admin_created",
				},
			},
			intent: "admin route is legit, but curated row was deleted; restore curated as fallback",
		},

		// Routing-class repairable cells.
		{
			cell: cellRoutingAdoptManagedSeed,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 0, rowExists: true,
			},
			intent: "curated row exists managed, but wildcard never seeded; adopt managed_seed at curated",
		},
		{
			cell: cellRoutingAdoptManagedSeedAdminOwned,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 0, rowExists: true, rowAdminOwned: true,
				adminDefaultCount: 1, adminDefaultID: testCuratedID, adminDefaultIsCurated: true,
			},
			intent: "admin took over the curated row but never persisted a wildcard; adopt managed_seed preserving the admin-owned row",
		},
		{
			cell: cellRoutingTakeoverFromNonAdmin,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 2, assignmentCount: 1, rowExists: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: "SomePlatformLayout", TargetPresent: true,
					RouteSource: opaqueRoute(),
				},
			},
			intent: "wildcard points at non-admin platform layout via opaque route, pack-managed curated row; take over to curated",
		},
		{
			cell: cellRoutingTakeoverFromNonAdminAdminOwned,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 2, assignmentCount: 1, rowExists: true, rowAdminOwned: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: "SomePlatformLayout", TargetPresent: true,
					RouteSource: opaqueRoute(),
				},
			},
			intent: "wildcard points at non-admin platform layout, curated row admin-owned; adopt managed_seed wildcard at curated while preserving the L2 row",
		},
		{
			cell: cellRoutingCanonicalizeAdminRoute,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 2, assignmentCount: 1, rowExists: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: "AdminCustomLayout", TargetPresent: true,
					RouteSource:  opaqueRoute(),
					IsCustomized: true, Source: "admin_created",
				},
			},
			intent: "admin-owned target via legacy route source, pack-managed curated row; canonicalize route to admin_authored and upsert curated as fallback",
		},
		{
			cell: cellRoutingCanonicalizeAdminRouteAdminOwned,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 2, assignmentCount: 1, rowExists: true, rowAdminOwned: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: "AdminCustomLayout", TargetPresent: true,
					RouteSource:  opaqueRoute(),
					IsCustomized: true, Source: "admin_created",
				},
			},
			intent: "admin-owned target and admin-owned curated row, legacy route source; canonicalize route to admin_authored at target without overwriting the L2 curated row",
		},
		{
			cell: cellRoutingRepairOpaqueSource,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 1, rowExists: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: testCuratedID, TargetPresent: true,
					RouteSource: opaqueRoute(),
				},
			},
			intent: "wildcard at curated but RouteSource is opaque, row pack-managed; reseat route to managed_seed",
		},
		{
			cell: cellRoutingRepairOpaqueSourceAdminOwned,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 1, assignmentCount: 1, rowExists: true, rowAdminOwned: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: testCuratedID, TargetPresent: true,
					RouteSource:  opaqueRoute(),
					IsCustomized: true, Source: "admin_created",
				},
			},
			intent: "wildcard at curated but RouteSource is opaque, row admin-owned; canonicalize route to admin_authored at curated, preserve L2 row content",
		},

		// Conflict cells — non-repairable.
		{
			cell: cellConflictMultipleWildcards,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				assignmentCount: 2, rowExists: true,
				wildcardTarget: &layoutownership.WildcardLayoutTarget{
					LayoutID: testCuratedID, TargetPresent: true,
					RouteSource: managedSeedRoute(),
				},
			},
			intent: "multiple wildcard rows is structurally ambiguous; operator must dedupe",
		},
		{
			cell: cellConflictMultipleAdminDefaults,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 3, assignmentCount: 0, rowExists: true,
				adminDefaultCount: 2, adminDefaultID: "LegacyA",
			},
			intent: "two admin-owned default rows without wildcard; operator picks",
		},
		{
			cell: cellConflictLegacyAdminDefault,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 2, assignmentCount: 0, rowExists: true,
				adminDefaultCount: 1, adminDefaultID: "LegacyAdminLayout", adminDefaultIsCurated: false,
			},
			intent: "legacy admin default elsewhere with no wildcard; operator decides whether to demote or persist as admin_authored wildcard",
		},
		{
			cell: cellConflictOrphanWildcardsWithRows,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				liveLayoutCount: 2, assignmentCount: 1, rowExists: true,
				// wildcardTarget nil → orphan
			},
			intent: "orphan wildcard with live rows present; not the safe greenfield-recover shape",
		},
		{
			cell: cellConflictAdminWildcardsWithoutTarget,
			seed: observedCuratedLayoutState{
				curatedID: testCuratedID, objectAPIName: testObjectAPIName,
				assignmentCount: 1, adminWildcardCount: 1,
				// wildcardTarget nil → admin-authored orphan route
			},
			intent: "admin-authored wildcard exists but no live target; operator must resolve L2 route",
		},
	}
}

// TestStateMatrix_ClassifyMatchesIntent proves classifyCuratedLayout is
// stable for each fixture: the seed for cell X classifies as cell X.
func TestStateMatrix_ClassifyMatchesIntent(t *testing.T) {
	for _, exp := range cellExpectations() {
		t.Run(exp.cell.name(), func(t *testing.T) {
			got := classifyCuratedLayout(&exp.seed)
			if got != exp.cell {
				t.Fatalf("classify(seed for %s) = %s, want %s\n  intent: %s",
					exp.cell.name(), got.name(), exp.cell.name(), exp.intent)
			}
		})
	}
}

// TestStateMatrix_EveryCellHasEntry catches the failure mode where a new
// cellXxx constant is added but the matrix forgets to wire it.
func TestStateMatrix_EveryCellHasEntry(t *testing.T) {
	seen := make(map[curatedLayoutCell]bool)
	for _, exp := range cellExpectations() {
		seen[exp.cell] = true
		if _, ok := stateMatrix[exp.cell]; !ok {
			t.Errorf("cell %s has a test fixture but no stateMatrix entry", exp.cell.name())
		}
	}
	for cell := range stateMatrix {
		if !seen[cell] && cell != cellUnmodeledFailClosed {
			t.Errorf("cell %s exists in stateMatrix but has no test fixture in cellExpectations()", cell.name())
		}
	}
}

// TestStateMatrix_RepairableCellsConvergePostApply proves apply
// convergence: every cell whose verify is repairable maps to an apply
// op whose verifyAfterApply is a vrOK cell. (Actually executing apply
// against a live DB and re-classifying is covered by integration tests
// in source_pack_runtime_recovery_integration_test.go; this test pins
// the contract at the matrix layer.)
func TestStateMatrix_RepairableCellsConvergePostApply(t *testing.T) {
	for cell, plan := range stateMatrix {
		if cell == cellUnmodeledFailClosed {
			continue
		}
		switch plan.verify {
		case vrMissing, vrRoutingDrift:
			postPlan, ok := stateMatrix[plan.apply.verifyAfterApply]
			if !ok {
				t.Errorf("cell %s repairable but verifyAfterApply=%s has no matrix entry",
					cell.name(), plan.apply.verifyAfterApply.name())
				continue
			}
			if postPlan.verify != vrOK {
				t.Errorf("cell %s repairable but post-apply cell %s has verify=%d (want vrOK)",
					cell.name(), plan.apply.verifyAfterApply.name(), int(postPlan.verify))
			}
			if plan.apply.op == applyNoop {
				t.Errorf("cell %s is repairable but apply op is noop", cell.name())
			}
		case vrConflict:
			if plan.apply.op != applyConflict {
				t.Errorf("cell %s is conflict but apply op is not applyConflict", cell.name())
			}
			if plan.apply.verifyAfterApply != cell {
				t.Errorf("cell %s is conflict; verifyAfterApply should be the same cell (no convergence), got %s",
					cell.name(), plan.apply.verifyAfterApply.name())
			}
		case vrOK:
			if plan.apply.op != applyNoop &&
				plan.apply.op != applyRefreshManagedCuratedAtCurated &&
				plan.apply.op != applyUpsertCuratedClearDefaultFlag &&
				plan.apply.op != applyClearCuratedDefaultFlagOnly &&
				plan.apply.op != applyMergePackRecordPanelsPreservingRow &&
				plan.apply.op != applyMergePackRecordPanelsAndClearDefaultPreservingRow {
				t.Errorf("cell %s is OK but apply op %d is not an OK-state apply op", cell.name(), int(plan.apply.op))
			}
		}
	}
}

// TestKnownCodexBLOCKs_HaveCells locks in the five state-matrix cells
// flagged by Codex across PR #512 R1 + PR #614 R1+R2, so the substrate
// proves it covers the patch-loop's underlying gaps explicitly.
func TestKnownCodexBLOCKs_HaveCells(t *testing.T) {
	// PR #614 Round 1 BLOCK #1 (curated_page_layout_reconcile.go:130 in
	// pr614 head before fix): wildcard managed_seed at curated row; if
	// config drifts, apply must converge. In the state machine this is
	// cellOKManagedSeedAtCurated for routing; config-drift is a
	// separate axis handled by verifyCuratedPageLayoutConfig.
	if _, ok := stateMatrix[cellOKManagedSeedAtCurated]; !ok {
		t.Error("PR #614 R1 BLOCK #1: missing cellOKManagedSeedAtCurated")
	}

	// PR #614 Round 1 BLOCK #2 (curated_page_layout_reconcile.go:308):
	// orphan wildcard + missing curated layout — should be repairable
	// when the rest of the state is greenfield-shaped.
	if _, ok := stateMatrix[cellMissingOrphanWildcardGreenfield]; !ok {
		t.Error("PR #614 R1 BLOCK #2: missing cellMissingOrphanWildcardGreenfield")
	}

	// PR #614 Round 2 BLOCK #1 (curated_page_layout_reconcile.go:149):
	// admin-authored wildcard at admin-owned other layout, curated row
	// exists managed with drift — apply must upsert curated even when
	// preserving the route.
	if _, ok := stateMatrix[cellOKAdminAuthoredAtAdminOther]; !ok {
		t.Error("PR #614 R2 BLOCK #1: missing cellOKAdminAuthoredAtAdminOther")
	}

	// PR #614 Round 2 BLOCK #2 (curated_page_layout_reconcile.go:341):
	// curated row that is itself admin-owned without a wildcard —
	// must be classified as repairable, apply adopts managed_seed at
	// the admin-owned row.
	if _, ok := stateMatrix[cellRoutingAdoptManagedSeedAdminOwned]; !ok {
		t.Error("PR #614 R2 BLOCK #2: missing cellRoutingAdoptManagedSeedAdminOwned")
	}

	// PR #512 R1: wildcard points at non-admin platform layout via
	// opaque/non-admin route — take over to managed_seed at curated.
	if _, ok := stateMatrix[cellRoutingTakeoverFromNonAdmin]; !ok {
		t.Error("PR #512 R1: missing cellRoutingTakeoverFromNonAdmin")
	}
}

// TestStateMatrix_VerifyResultStringification keeps the cell names from
// silently drifting from the constant names — humans grep for these in
// log lines and Codex BLOCK descriptions.
func TestStateMatrix_VerifyResultStringification(t *testing.T) {
	for cell := range stateMatrix {
		name := cell.name()
		if strings.HasPrefix(name, "cell(") {
			t.Errorf("cell value %d falls through to default name; add a name() case", int(cell))
		}
	}
}
