package catalogpublication

import (
	"strings"
	"testing"

	catalogpublicationv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalogpublicationv1"
)

func TestValidateSelectionTransitionAcceptsInitialAndIndependentAxisSteps(t *testing.T) {
	initial := transitionBinding(1, 1)
	if err := validateSelectionTransition(0, 0, nil, initial); err != nil {
		t.Fatalf("initial transition: %v", err)
	}

	catalogOnly := initial
	catalogOnly.CatalogRevision++
	catalogOnly.CatalogSnapshotDigest = transitionDigest("5")
	catalogOnly.SchemaGeneration++
	catalogOnly.EngineGeneration++
	catalogOnly.LayoutValidationViewDigest = transitionDigest("6")
	catalogOnly.LayoutActivationSetDigest = transitionDigest("7")
	if err := validateSelectionTransition(1, 1, &initial, catalogOnly); err != nil {
		t.Fatalf("catalog-only transition: %v", err)
	}

	securityOnly := initial
	securityOnly.SecurityEpoch++
	securityOnly.LayoutRoutingSecurityViewDigest = transitionDigest("8")
	securityOnly.LayoutActivationSetDigest = transitionDigest("9")
	if err := validateSelectionTransition(1, 1, &initial, securityOnly); err != nil {
		t.Fatalf("security-only transition: %v", err)
	}

	combined := initial
	combined.CatalogRevision++
	combined.SecurityEpoch++
	combined.CatalogSnapshotDigest = transitionDigest("5")
	combined.LayoutRoutingSecurityViewDigest = transitionDigest("8")
	if err := validateSelectionTransition(1, 1, &initial, combined); err != nil {
		t.Fatalf("combined transition: %v", err)
	}
}

func TestValidateSelectionTransitionRejectsIncoherentInitialState(t *testing.T) {
	tests := []struct {
		name              string
		writerCatalog     uint64
		writerSecurity    uint64
		candidateCatalog  uint64
		candidateSecurity uint64
		want              string
	}{
		{name: "writer catalog already advanced", writerCatalog: 1, candidateCatalog: 1, candidateSecurity: 1, want: "zero writer state"},
		{name: "writer security already advanced", writerSecurity: 1, candidateCatalog: 1, candidateSecurity: 1, want: "zero writer state"},
		{name: "catalog starts above one", candidateCatalog: 2, candidateSecurity: 1, want: "from zero to one"},
		{name: "security starts above one", candidateCatalog: 1, candidateSecurity: 2, want: "from zero to one"},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			candidate := transitionBinding(test.candidateCatalog, test.candidateSecurity)
			assertTransitionError(t, test.want, validateSelectionTransition(test.writerCatalog, test.writerSecurity, nil, candidate))
		})
	}
}

func TestValidateSelectionTransitionRejectsAxisJumpRegressionAndNoop(t *testing.T) {
	active := transitionBinding(7, 11)
	tests := []struct {
		name   string
		mutate func(*catalogpublicationv1.Binding)
		want   string
	}{
		{name: "no axis advances", mutate: func(*catalogpublicationv1.Binding) {}, want: "must advance"},
		{name: "catalog jumps", mutate: func(value *catalogpublicationv1.Binding) { value.CatalogRevision += 2 }, want: "CatalogRevision jumped"},
		{name: "catalog regresses", mutate: func(value *catalogpublicationv1.Binding) { value.CatalogRevision-- }, want: "CatalogRevision regressed"},
		{name: "security jumps", mutate: func(value *catalogpublicationv1.Binding) { value.SecurityEpoch += 2 }, want: "SecurityEpoch jumped"},
		{name: "security regresses", mutate: func(value *catalogpublicationv1.Binding) { value.SecurityEpoch-- }, want: "SecurityEpoch regressed"},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			candidate := active
			test.mutate(&candidate)
			assertTransitionError(t, test.want, validateSelectionTransition(7, 11, &active, candidate))
		})
	}
}

func TestValidateSelectionTransitionRejectsWriterAndBindingMismatch(t *testing.T) {
	active := transitionBinding(7, 11)
	candidate := active
	candidate.CatalogRevision++

	assertTransitionError(t, "writer state does not match", validateSelectionTransition(6, 11, &active, candidate))
	assertTransitionError(t, "writer state does not match", validateSelectionTransition(7, 10, &active, candidate))

	candidate.OrgID = "org-2"
	assertTransitionError(t, "does not match active OrgID", validateSelectionTransition(7, 11, &active, candidate))
}

func TestValidateSelectionTransitionRejectsChangesBehindRetainedAxis(t *testing.T) {
	active := transitionBinding(7, 11)

	for name, mutate := range map[string]func(*catalogpublicationv1.Binding){
		"snapshot digest":   func(value *catalogpublicationv1.Binding) { value.CatalogSnapshotDigest = transitionDigest("5") },
		"schema generation": func(value *catalogpublicationv1.Binding) { value.SchemaGeneration++ },
		"engine generation": func(value *catalogpublicationv1.Binding) { value.EngineGeneration++ },
		"validation view":   func(value *catalogpublicationv1.Binding) { value.LayoutValidationViewDigest = transitionDigest("6") },
	} {
		t.Run(name, func(t *testing.T) {
			candidate := active
			candidate.SecurityEpoch++
			candidate.LayoutActivationSetDigest = transitionDigest("9")
			mutate(&candidate)
			assertTransitionError(t, "retained CatalogRevision", validateSelectionTransition(7, 11, &active, candidate))
		})
	}

	catalogOnly := active
	catalogOnly.CatalogRevision++
	catalogOnly.LayoutRoutingSecurityViewDigest = transitionDigest("8")
	assertTransitionError(t, "retained SecurityEpoch", validateSelectionTransition(7, 11, &active, catalogOnly))
}

func TestValidateSelectionTransitionRequiresFreshActivationSetForSecurityOnlyChange(t *testing.T) {
	active := transitionBinding(7, 11)
	candidate := active
	candidate.SecurityEpoch++
	candidate.LayoutRoutingSecurityViewDigest = transitionDigest("8")

	assertTransitionError(t, "new LayoutActivationSetDigest", validateSelectionTransition(7, 11, &active, candidate))
}

func TestValidateSelectionTransitionValidatesCompleteBindings(t *testing.T) {
	invalidCandidate := transitionBinding(1, 1)
	invalidCandidate.CatalogSnapshotDigest = "invalid"
	assertTransitionError(t, "candidate binding", validateSelectionTransition(0, 0, nil, invalidCandidate))

	invalidActive := transitionBinding(7, 11)
	invalidActive.LayoutActivationSetDigest = "invalid"
	candidate := transitionBinding(8, 11)
	assertTransitionError(t, "active binding", validateSelectionTransition(7, 11, &invalidActive, candidate))
}

func TestValidateStoppedWriterAdoptionAcceptsCurrentInitialAndExactStep(t *testing.T) {
	initial := transitionBinding(7, 11)
	if err := validateStoppedWriterAdoption(7, 11, nil, initial); err != nil {
		t.Fatalf("initial stopped adoption: %v", err)
	}
	if err := validateStoppedWriterAdoption(7, 11, &initial, initial); err != nil {
		t.Fatalf("unchanged stopped adoption: %v", err)
	}

	successor := initial
	successor.CatalogRevision++
	successor.CatalogSnapshotDigest = transitionDigest("5")
	successor.SchemaGeneration++
	successor.EngineGeneration++
	successor.LayoutValidationViewDigest = transitionDigest("6")
	successor.LayoutActivationSetDigest = transitionDigest("7")
	if err := validateStoppedWriterAdoption(8, 11, &initial, successor); err != nil {
		t.Fatalf("successor stopped adoption: %v", err)
	}
}

func TestValidateStoppedWriterAdoptionRejectsMixedOrJumpedLineage(t *testing.T) {
	active := transitionBinding(7, 11)
	mixed := active
	mixed.CatalogRevision = 8
	mixed.CatalogSnapshotDigest = transitionDigest("5")
	mixed.LayoutValidationViewDigest = transitionDigest("6")
	mixed.LayoutActivationSetDigest = transitionDigest("7")
	assertTransitionError(
		t,
		"does not match current writer state",
		validateStoppedWriterAdoption(7, 11, &active, mixed),
	)

	jumped := mixed
	jumped.CatalogRevision = 9
	assertTransitionError(
		t,
		"jumped from 7 to 9",
		validateStoppedWriterAdoption(9, 11, &active, jumped),
	)

	retained := active
	retained.LayoutValidationViewDigest = transitionDigest("6")
	retained.LayoutActivationSetDigest = transitionDigest("7")
	assertTransitionError(
		t,
		"publication must advance",
		validateStoppedWriterAdoption(7, 11, &active, retained),
	)
}

func transitionBinding(catalogRevision, securityEpoch uint64) catalogpublicationv1.Binding {
	return catalogpublicationv1.Binding{
		OrgID:                           "org-1",
		CatalogRevision:                 catalogRevision,
		CatalogSnapshotDigest:           transitionDigest("1"),
		SchemaGeneration:                2,
		EngineGeneration:                3,
		LayoutValidationViewDigest:      transitionDigest("2"),
		LayoutActivationSetDigest:       transitionDigest("3"),
		SecurityEpoch:                   securityEpoch,
		LayoutRoutingSecurityViewDigest: transitionDigest("4"),
	}
}

func transitionDigest(digit string) string {
	return "sha256:" + strings.Repeat(digit, 64)
}

func assertTransitionError(t *testing.T, want string, err error) {
	t.Helper()
	if err == nil || !strings.Contains(err.Error(), want) {
		t.Fatalf("error = %v, want substring %q", err, want)
	}
}
