package systemlayoutbundleauthorityv1

import (
	"bytes"
	"math"
	"strings"
	"testing"

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

func TestSlotValuesAcceptOnlyExactSystemGrammar(t *testing.T) {
	for _, state := range []SlotState{SlotStateActive, SlotStateTombstone} {
		value := validSlot(state, "1")
		if err := ValidateSlotValue(value); err != nil {
			t.Fatalf("ValidateSlotValue(%s): %v", state, err)
		}
		if canonical, err := CanonicalSlotValueBytes(value); err != nil || len(canonical) == 0 {
			t.Fatalf("CanonicalSlotValueBytes(%s) = %x, %v", state, canonical, err)
		}
		if digest, err := SlotValueDigest(value); err != nil || !strings.HasPrefix(digest, "sha256:") {
			t.Fatalf("SlotValueDigest(%s) = %q, %v", state, digest, err)
		}
	}

	tests := map[string]func(*SystemLayoutBundleSlotV1){
		"value version": func(value *SystemLayoutBundleSlotV1) { value.ValueVersion = SlotValueVersion + " " },
		"bundle key version": func(value *SystemLayoutBundleSlotV1) {
			value.BundleKey.BundleKeyVersion = "nexuscrm.layout-bundle-key/v2"
		},
		"app owner": func(value *SystemLayoutBundleSlotV1) {
			value.BundleKey.SlotOwnerKind = layoutbundlekeyv1.SlotOwnerKindApp
		},
		"tenant owner": func(value *SystemLayoutBundleSlotV1) {
			value.BundleKey.SlotOwnerKind = layoutbundlekeyv1.SlotOwnerKindTenant
		},
		"alternate system namespace": func(value *SystemLayoutBundleSlotV1) {
			value.BundleKey.SlotOwnerNamespace = "io.nexuscrm.other"
		},
		"uppercase key": func(value *SystemLayoutBundleSlotV1) {
			value.BundleKey.LocalComponentKey = layoutbundlekeyv1.LocalComponentKey("key256:" + strings.Repeat("A", 64))
		},
		"short key": func(value *SystemLayoutBundleSlotV1) {
			value.BundleKey.LocalComponentKey = layoutbundlekeyv1.LocalComponentKey("key256:" + strings.Repeat("1", 63))
		},
		"absent is not a state": func(value *SystemLayoutBundleSlotV1) { value.State = "ABSENT" },
		"unknown state":         func(value *SystemLayoutBundleSlotV1) { value.State = "active" },
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			value := validSlot(SlotStateActive, "1")
			mutate(&value)
			assertInvalidSlot(t, value)
		})
	}
}

func TestCommandIDPinsLowercaseCanonicalUUIDv7(t *testing.T) {
	for _, value := range []CommandID{
		"01890f9e-7b2a-7000-8000-000000000001",
		"ffffffff-ffff-7fff-bfff-ffffffffffff",
		"00000000-0000-7000-9000-000000000000",
	} {
		if err := ValidateCommandID(value); err != nil {
			t.Fatalf("ValidateCommandID(%q): %v", value, err)
		}
	}
	for _, value := range []CommandID{
		"",
		"01890F9E-7B2A-7000-8000-000000000001",
		"01890f9e-7b2a-4000-8000-000000000001",
		"01890f9e-7b2a-7000-7000-000000000001",
		"01890f9e-7b2a-7000-c000-000000000001",
		"01890f9e7b2a-7000-8000-000000000001",
		"01890f9e-7b2a-7000-8000-00000000000g",
		" 01890f9e-7b2a-7000-8000-000000000001",
	} {
		if err := ValidateCommandID(value); err == nil {
			t.Fatalf("ValidateCommandID(%q) succeeded", value)
		}
	}
}

func TestCommandRequestsAcceptOnlyCreateAbsentAndRetireExact(t *testing.T) {
	create := validCreateRequest("1")
	if err := ValidateCommandRequest(create); err != nil {
		t.Fatal(err)
	}
	retire := validRetireRequest(t, "1")
	if err := ValidateCommandRequest(retire); err != nil {
		t.Fatal(err)
	}
	createCanonical, err := CanonicalCommandRequestBytes(create)
	if err != nil {
		t.Fatal(err)
	}
	retireCanonical, err := CanonicalCommandRequestBytes(retire)
	if err != nil {
		t.Fatal(err)
	}
	if bytes.Equal(createCanonical, retireCanonical) {
		t.Fatal("CREATE+ABSENT and RETIRE+EXACT have identical canonical bytes")
	}
	if createCanonical[len(createCanonical)-1] != 0x00 {
		t.Fatalf("CREATE expected-prior tag = 0x%02x", createCanonical[len(createCanonical)-1])
	}
	priorDigestLength := 1 + len(retire.ExpectedPrior.SlotValueDigest)
	priorTagOffset := len(retireCanonical) - priorDigestLength - 1
	if retireCanonical[priorTagOffset] != 0x01 {
		t.Fatalf("RETIRE expected-prior tag = 0x%02x", retireCanonical[priorTagOffset])
	}

	tests := map[string]func(*SystemLayoutBundleSlotCommandRequest){
		"unknown operation": func(request *SystemLayoutBundleSlotCommandRequest) { request.Operation = "UPSERT" },
		"target desired mismatch": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.TargetBundleKey.LocalComponentKey = key("2")
		},
		"create tombstone": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.DesiredSlotValue.State = SlotStateTombstone
		},
		"create exact": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.ExpectedPrior = ExpectedPrior{Variant: ExpectedPriorExact, SlotValueDigest: digest("1")}
		},
		"create absent with digest": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.ExpectedPrior.SlotValueDigest = digest("1")
		},
		"create unknown prior": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.ExpectedPrior.Variant = "NONE"
		},
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			request := validCreateRequest("1")
			mutate(&request)
			assertInvalidRequest(t, request)
		})
	}

	retireTests := map[string]func(*SystemLayoutBundleSlotCommandRequest){
		"retire active": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.DesiredSlotValue.State = SlotStateActive
		},
		"retire absent": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.ExpectedPrior = ExpectedPrior{Variant: ExpectedPriorAbsent}
		},
		"retire empty exact digest": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.ExpectedPrior.SlotValueDigest = ""
		},
		"retire uppercase exact digest": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.ExpectedPrior.SlotValueDigest = digest("A")
		},
		"retire wrong active digest": func(request *SystemLayoutBundleSlotCommandRequest) {
			request.ExpectedPrior.SlotValueDigest = digest("9")
		},
	}
	for name, mutate := range retireTests {
		t.Run(name, func(t *testing.T) {
			request := validRetireRequest(t, "1")
			mutate(&request)
			assertInvalidRequest(t, request)
		})
	}
}

func TestCommandIDIsNotRequestDigestInput(t *testing.T) {
	request := validCreateRequest("1")
	digestValue, err := CommandRequestDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	for _, commandID := range []CommandID{
		"01890f9e-7b2a-7000-8000-000000000001",
		"01890f9e-7b2a-7000-8000-000000000002",
	} {
		if err := ValidateCommandID(commandID); err != nil {
			t.Fatal(err)
		}
		got, err := CommandRequestDigest(request)
		if err != nil || got != digestValue {
			t.Fatalf("request digest changed with independent CommandID %s: %q, %v", commandID, got, err)
		}
	}
}

func TestEvidenceAcceptsOnlyCompleteActiveSystemClaim(t *testing.T) {
	evidence := validEvidence("1")
	if err := ValidateEvidence(evidence); err != nil {
		t.Fatal(err)
	}
	if _, err := CanonicalEvidenceBytes(evidence); err != nil {
		t.Fatal(err)
	}
	if _, err := EvidenceDigest(evidence); err != nil {
		t.Fatal(err)
	}

	tests := map[string]func(*SystemLayoutBundleAuthorityEvidenceV1){
		"evidence version": func(value *SystemLayoutBundleAuthorityEvidenceV1) { value.EvidenceVersion = "v1" },
		"source org":       func(value *SystemLayoutBundleAuthorityEvidenceV1) { value.SourceCatalogSnapshot.OrgID = "" },
		"source revision": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.CatalogRevision = 0
		},
		"source digest": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.CatalogSnapshotDigest = digest("A")
		},
		"schema generation": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.SchemaGeneration = 0
		},
		"engine generation": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.EngineGeneration = 0
		},
		"non-system key": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.BundleKey.SlotOwnerKind = layoutbundlekeyv1.SlotOwnerKindTenant
		},
		"slot value version": func(value *SystemLayoutBundleAuthorityEvidenceV1) { value.SlotValueVersion = "v1" },
		"tombstone":          func(value *SystemLayoutBundleAuthorityEvidenceV1) { value.SlotState = SlotStateTombstone },
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			value := validEvidence("1")
			mutate(&value)
			assertInvalidEvidence(t, value)
		})
	}
}

func TestDigestsCommitEveryVariableCanonicalField(t *testing.T) {
	active := validSlot(SlotStateActive, "1")
	activeDigest, err := SlotValueDigest(active)
	if err != nil {
		t.Fatal(err)
	}
	for name, changed := range map[string]SystemLayoutBundleSlotV1{
		"BundleKey.LocalComponentKey": validSlot(SlotStateActive, "2"),
		"State":                       validSlot(SlotStateTombstone, "1"),
	} {
		t.Run("slot "+name, func(t *testing.T) {
			digestValue, err := SlotValueDigest(changed)
			if err != nil || digestValue == activeDigest {
				t.Fatalf("changed digest = %q, %v", digestValue, err)
			}
		})
	}

	baseRequest := validCreateRequest("1")
	baseRequestDigest, err := CommandRequestDigest(baseRequest)
	if err != nil {
		t.Fatal(err)
	}
	for name, changed := range map[string]SystemLayoutBundleSlotCommandRequest{
		"BundleKey":                         validCreateRequest("2"),
		"Operation Desired State and Prior": validRetireRequest(t, "1"),
	} {
		t.Run("request "+name, func(t *testing.T) {
			digestValue, err := CommandRequestDigest(changed)
			if err != nil || digestValue == baseRequestDigest {
				t.Fatalf("changed digest = %q, %v", digestValue, err)
			}
		})
	}

	baseEvidence := validEvidence("1")
	baseEvidenceDigest, err := EvidenceDigest(baseEvidence)
	if err != nil {
		t.Fatal(err)
	}
	mutations := map[string]func(*SystemLayoutBundleAuthorityEvidenceV1){
		"SourceCatalogSnapshot.OrgID": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.OrgID = "org-2"
		},
		"SourceCatalogSnapshot.CatalogRevision": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.CatalogRevision++
		},
		"SourceCatalogSnapshot.CatalogSnapshotDigest": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.CatalogSnapshotDigest = digest("9")
		},
		"SourceCatalogSnapshot.SchemaGeneration": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.SchemaGeneration++
		},
		"SourceCatalogSnapshot.EngineGeneration": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.SourceCatalogSnapshot.EngineGeneration++
		},
		"BundleKey.LocalComponentKey": func(value *SystemLayoutBundleAuthorityEvidenceV1) {
			value.BundleKey.LocalComponentKey = key("2")
		},
	}
	for name, mutate := range mutations {
		t.Run("evidence "+name, func(t *testing.T) {
			changed := baseEvidence
			mutate(&changed)
			digestValue, err := EvidenceDigest(changed)
			if err != nil || digestValue == baseEvidenceDigest {
				t.Fatalf("changed digest = %q, %v", digestValue, err)
			}
		})
	}
}

func TestCanonicalResultsAreDetachedFromInputMutation(t *testing.T) {
	slot := validSlot(SlotStateActive, "1")
	canonicalSlot, err := CanonicalSlotValueBytes(slot)
	if err != nil {
		t.Fatal(err)
	}
	savedSlot := append([]byte(nil), canonicalSlot...)
	slot.BundleKey.LocalComponentKey = key("2")
	if !bytes.Equal(canonicalSlot, savedSlot) {
		t.Fatal("slot input mutation altered previously returned canonical bytes")
	}

	request := validCreateRequest("1")
	canonicalRequest, err := CanonicalCommandRequestBytes(request)
	if err != nil {
		t.Fatal(err)
	}
	savedRequest := append([]byte(nil), canonicalRequest...)
	request.TargetBundleKey.LocalComponentKey = key("2")
	request.DesiredSlotValue.BundleKey.LocalComponentKey = key("2")
	if !bytes.Equal(canonicalRequest, savedRequest) {
		t.Fatal("request input mutation altered previously returned canonical bytes")
	}

	evidence := validEvidence("1")
	canonicalEvidence, err := CanonicalEvidenceBytes(evidence)
	if err != nil {
		t.Fatal(err)
	}
	savedEvidence := append([]byte(nil), canonicalEvidence...)
	evidence.SourceCatalogSnapshot.OrgID = "org-2"
	if !bytes.Equal(canonicalEvidence, savedEvidence) {
		t.Fatal("evidence input mutation altered previously returned canonical bytes")
	}
}

func TestEvidenceBoundsAndMultibyteUnsignedLEB128(t *testing.T) {
	value := validEvidence("1")
	value.SourceCatalogSnapshot.OrgID = strings.Repeat("o", catalogpublicationv1.MaxOrgIDBytes)
	value.SourceCatalogSnapshot.CatalogRevision = 127
	value.SourceCatalogSnapshot.SchemaGeneration = 128
	value.SourceCatalogSnapshot.EngineGeneration = math.MaxUint64
	if _, err := CanonicalEvidenceBytes(value); err != nil {
		t.Fatalf("maximum source bounds rejected: %v", err)
	}
	value.SourceCatalogSnapshot.OrgID += "o"
	assertInvalidEvidence(t, value)
}

func validBundleKey(character string) BundleKey {
	return BundleKey{
		BundleKeyVersion:   layoutbundlekeyv1.BundleKeyVersion,
		SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindSystem,
		SlotOwnerNamespace: layoutbundlekeyv1.SystemSlotOwnerNamespace,
		LocalComponentKey:  key(character),
	}
}

func validSlot(state SlotState, character string) SystemLayoutBundleSlotV1 {
	return SystemLayoutBundleSlotV1{
		ValueVersion: SlotValueVersion,
		BundleKey:    validBundleKey(character),
		State:        state,
	}
}

func validCreateRequest(character string) SystemLayoutBundleSlotCommandRequest {
	keyValue := validBundleKey(character)
	return SystemLayoutBundleSlotCommandRequest{
		Operation:        OperationCreate,
		TargetBundleKey:  keyValue,
		DesiredSlotValue: SystemLayoutBundleSlotV1{ValueVersion: SlotValueVersion, BundleKey: keyValue, State: SlotStateActive},
		ExpectedPrior:    ExpectedPrior{Variant: ExpectedPriorAbsent},
	}
}

func validRetireRequest(t *testing.T, character string) SystemLayoutBundleSlotCommandRequest {
	t.Helper()
	keyValue := validBundleKey(character)
	activeDigest, err := SlotValueDigest(SystemLayoutBundleSlotV1{
		ValueVersion: SlotValueVersion,
		BundleKey:    keyValue,
		State:        SlotStateActive,
	})
	if err != nil {
		t.Fatal(err)
	}
	return SystemLayoutBundleSlotCommandRequest{
		Operation:        OperationRetire,
		TargetBundleKey:  keyValue,
		DesiredSlotValue: SystemLayoutBundleSlotV1{ValueVersion: SlotValueVersion, BundleKey: keyValue, State: SlotStateTombstone},
		ExpectedPrior:    ExpectedPrior{Variant: ExpectedPriorExact, SlotValueDigest: activeDigest},
	}
}

func validEvidence(character string) SystemLayoutBundleAuthorityEvidenceV1 {
	return SystemLayoutBundleAuthorityEvidenceV1{
		EvidenceVersion: EvidenceVersion,
		SourceCatalogSnapshot: catalogpublicationv1.CatalogSnapshotBinding{
			OrgID:                 "org-1",
			CatalogRevision:       1,
			CatalogSnapshotDigest: digest("1"),
			SchemaGeneration:      2,
			EngineGeneration:      3,
		},
		BundleKey:        validBundleKey(character),
		SlotValueVersion: SlotValueVersion,
		SlotState:        SlotStateActive,
	}
}

func key(character string) layoutbundlekeyv1.LocalComponentKey {
	return layoutbundlekeyv1.LocalComponentKey("key256:" + strings.Repeat(character, 64))
}

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

func assertInvalidSlot(t *testing.T, value SystemLayoutBundleSlotV1) {
	t.Helper()
	if err := ValidateSlotValue(value); err == nil {
		t.Fatal("ValidateSlotValue accepted invalid value")
	}
	if canonical, err := CanonicalSlotValueBytes(value); err == nil || canonical != nil {
		t.Fatalf("invalid slot acquired canonical bytes %x, %v", canonical, err)
	}
	if digestValue, err := SlotValueDigest(value); err == nil || digestValue != "" {
		t.Fatalf("invalid slot acquired digest %q, %v", digestValue, err)
	}
}

func assertInvalidRequest(t *testing.T, request SystemLayoutBundleSlotCommandRequest) {
	t.Helper()
	if err := ValidateCommandRequest(request); err == nil {
		t.Fatal("ValidateCommandRequest accepted invalid request")
	}
	if canonical, err := CanonicalCommandRequestBytes(request); err == nil || canonical != nil {
		t.Fatalf("invalid request acquired canonical bytes %x, %v", canonical, err)
	}
	if digestValue, err := CommandRequestDigest(request); err == nil || digestValue != "" {
		t.Fatalf("invalid request acquired digest %q, %v", digestValue, err)
	}
}

func assertInvalidEvidence(t *testing.T, value SystemLayoutBundleAuthorityEvidenceV1) {
	t.Helper()
	if err := ValidateEvidence(value); err == nil {
		t.Fatal("ValidateEvidence accepted invalid evidence")
	}
	if canonical, err := CanonicalEvidenceBytes(value); err == nil || canonical != nil {
		t.Fatalf("invalid evidence acquired canonical bytes %x, %v", canonical, err)
	}
	if digestValue, err := EvidenceDigest(value); err == nil || digestValue != "" {
		t.Fatalf("invalid evidence acquired digest %q, %v", digestValue, err)
	}
}
