package layoutactivationsetv1

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"strconv"
	"strings"
	"testing"

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

func TestImmutableConformanceVectors(t *testing.T) {
	var document activationVectorDocument
	decoder := json.NewDecoder(bytes.NewReader(ConformanceVectorsJSON()))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&document); err != nil {
		t.Fatal(err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		t.Fatalf("unexpected trailing JSON: %v", err)
	}
	if document.SchemaVersion != "nexuscrm.layout-activation-set.conformance/v1" {
		t.Fatalf("schema_version = %q", document.SchemaVersion)
	}
	if document.SetVectors == nil || document.CountBoundaryVectors == nil || document.RejectionVectors == nil {
		t.Fatal("all vector collections must be present JSON arrays")
	}
	if len(document.SetVectors) != 3 || len(document.CountBoundaryVectors) != 2 || len(document.RejectionVectors) != 2 {
		t.Fatalf(
			"vector counts = %d/%d/%d, want 3/2/2",
			len(document.SetVectors),
			len(document.CountBoundaryVectors),
			len(document.RejectionVectors),
		)
	}

	seenNames := map[string]bool{}
	setVectorsByName := make(map[string]activationSetVector, len(document.SetVectors))
	for _, vector := range document.SetVectors {
		assertUniqueActivationVectorName(t, seenNames, vector.Name)
		setVectorsByName[vector.Name] = vector
	}
	for _, vector := range document.CountBoundaryVectors {
		assertUniqueActivationVectorName(t, seenNames, vector.Name)
	}
	for _, vector := range document.RejectionVectors {
		assertUniqueActivationVectorName(t, seenNames, vector.Name)
	}

	for _, vector := range document.SetVectors {
		t.Run(vector.Name, func(t *testing.T) {
			set, err := vector.LayoutActivationSet.value()
			if err != nil {
				t.Fatal(err)
			}
			before := cloneActivationSet(set)
			canonical := assertActivationVectorIdentity(
				t,
				set,
				vector.CanonicalBytesHex,
				vector.LayoutActivationSetDigest,
			)
			if !reflect.DeepEqual(set, before) {
				t.Fatal("canonicalization or digesting mutated caller-owned input")
			}
			if vector.CanonicalEquivalentTo == "" {
				return
			}
			if vector.CanonicalEquivalentTo == vector.Name {
				t.Fatal("canonical_equivalent_to must not reference the vector itself")
			}
			reference, ok := setVectorsByName[vector.CanonicalEquivalentTo]
			if !ok {
				t.Fatalf("unknown canonical_equivalent_to %q", vector.CanonicalEquivalentTo)
			}
			referenceSet, err := reference.LayoutActivationSet.value()
			if err != nil {
				t.Fatal(err)
			}
			referenceCanonical, err := CanonicalSetBytes(referenceSet)
			if err != nil {
				t.Fatal(err)
			}
			if !bytes.Equal(canonical, referenceCanonical) || vector.LayoutActivationSetDigest != reference.LayoutActivationSetDigest {
				t.Fatalf("vector is not canonically equivalent to %q", vector.CanonicalEquivalentTo)
			}
		})
	}

	for _, vector := range document.CountBoundaryVectors {
		t.Run(vector.Name, func(t *testing.T) {
			set, err := vector.value()
			if err != nil {
				t.Fatal(err)
			}
			before := cloneActivationSet(set)
			canonical, err := CanonicalSetBytes(set)
			if err != nil {
				t.Fatal(err)
			}
			assertActivationVectorDigest(t, set, canonical, vector.LayoutActivationSetDigest)
			if !reflect.DeepEqual(set, before) {
				t.Fatal("count-boundary canonicalization or digesting mutated caller-owned input")
			}

			wantLength, err := vector.CanonicalBytesLength.required("canonical_bytes_length")
			if err != nil {
				t.Fatal(err)
			}
			if uint64(len(canonical)) != wantLength {
				t.Fatalf("canonical length = %d, want %d", len(canonical), wantLength)
			}

			countBytes := mustDecodeActivationVectorHex(t, vector.CanonicalCountBytesHex)
			countOffset := activationEntryCountOffset(set)
			if countOffset > len(canonical) || len(countBytes) > len(canonical)-countOffset {
				t.Fatalf("entry-count bytes fall outside canonical bytes at offset %d", countOffset)
			}
			if !bytes.Equal(canonical[countOffset:countOffset+len(countBytes)], countBytes) {
				t.Fatalf("entry-count bytes = %x, want %x", canonical[countOffset:countOffset+len(countBytes)], countBytes)
			}
		})
	}

	for _, vector := range document.RejectionVectors {
		t.Run(vector.Name, func(t *testing.T) {
			if vector.ErrorContains == "" {
				t.Fatal("error_contains must not be empty")
			}
			set, err := vector.LayoutActivationSet.value()
			if err != nil {
				t.Fatal(err)
			}
			if _, err := CanonicalSetBytes(set); err == nil || !strings.Contains(err.Error(), vector.ErrorContains) {
				t.Fatalf("CanonicalSetBytes error = %v, want substring %q", err, vector.ErrorContains)
			}
			if digest, err := SetDigest(set); err == nil || digest != "" || !strings.Contains(err.Error(), vector.ErrorContains) {
				t.Fatalf("SetDigest = %q, error = %v, want substring %q", digest, err, vector.ErrorContains)
			}
		})
	}
}

func TestConformanceVectorsJSONReturnsDefensiveCopy(t *testing.T) {
	testvectors.AssertDefensiveCopy(t, ConformanceVectorsJSON)
}

func TestActivationVectorDecimalRejectsNonCanonicalJSON(t *testing.T) {
	for _, input := range []string{`"0"`, `"1"`, `"18446744073709551615"`} {
		var value activationVectorDecimal
		if err := json.Unmarshal([]byte(input), &value); err != nil {
			t.Errorf("json.Unmarshal(%s): %v", input, err)
		}
	}
	for _, input := range []string{
		`0`, `1`, `null`, `true`, `""`, `"00"`, `"01"`, `"+1"`, `"-1"`, `"1.0"`, `" 1"`, `"1 "`, `"18446744073709551616"`,
	} {
		var value activationVectorDecimal
		if err := json.Unmarshal([]byte(input), &value); err == nil {
			t.Errorf("json.Unmarshal(%s) unexpectedly succeeded", input)
		}
	}
}

type activationVectorDocument struct {
	SchemaVersion        string                          `json:"schema_version"`
	SetVectors           []activationSetVector           `json:"set_vectors"`
	CountBoundaryVectors []activationCountBoundaryVector `json:"count_boundary_vectors"`
	RejectionVectors     []activationRejectionVector     `json:"rejection_vectors"`
}

type activationSetVector struct {
	Name                      string              `json:"name"`
	CanonicalEquivalentTo     string              `json:"canonical_equivalent_to,omitempty"`
	LayoutActivationSet       activationVectorSet `json:"layout_activation_set"`
	CanonicalBytesHex         string              `json:"canonical_bytes_hex"`
	LayoutActivationSetDigest string              `json:"layout_activation_set_digest"`
}

type activationCountBoundaryVector struct {
	Name                      string                    `json:"name"`
	LayoutActivationSet       activationVectorSetHeader `json:"layout_activation_set"`
	EntryRecipe               activationEntryRecipe     `json:"entry_recipe"`
	CanonicalCountBytesHex    string                    `json:"canonical_count_bytes_hex"`
	CanonicalBytesLength      activationVectorDecimal   `json:"canonical_bytes_length"`
	LayoutActivationSetDigest string                    `json:"layout_activation_set_digest"`
}

type activationRejectionVector struct {
	Name                string              `json:"name"`
	LayoutActivationSet activationVectorSet `json:"layout_activation_set"`
	ErrorContains       string              `json:"error_contains"`
}

type activationVectorSet struct {
	activationVectorSetHeader
	Entries []activationVectorEntry `json:"entries"`
}

type activationVectorSetHeader struct {
	CanonicalizationVersion     string                          `json:"canonicalization_version"`
	OrgID                       string                          `json:"org_id"`
	CatalogSchema               activationVectorCatalogSchema   `json:"catalog_schema"`
	RoutingSecurity             activationVectorRoutingSecurity `json:"routing_security"`
	RequiredBundleKeySetDigest  string                          `json:"required_bundle_key_set_digest"`
	PreparationProvenanceDigest string                          `json:"preparation_provenance_digest"`
}

type activationVectorCatalogSchema struct {
	CatalogSnapshot            activationVectorCatalogSnapshot `json:"catalog_snapshot"`
	LayoutValidationViewDigest string                          `json:"layout_validation_view_digest"`
}

type activationVectorCatalogSnapshot struct {
	OrgID                 string                  `json:"org_id"`
	CatalogRevision       activationVectorDecimal `json:"catalog_revision"`
	CatalogSnapshotDigest string                  `json:"catalog_snapshot_digest"`
	SchemaGeneration      activationVectorDecimal `json:"schema_generation"`
	EngineGeneration      activationVectorDecimal `json:"engine_generation"`
}

type activationVectorRoutingSecurity struct {
	OrgID                           string                  `json:"org_id"`
	SecurityEpoch                   activationVectorDecimal `json:"security_epoch"`
	LayoutRoutingSecurityViewDigest string                  `json:"layout_routing_security_view_digest"`
}

type activationVectorEntry struct {
	BundleKey                activationVectorBundleKey `json:"bundle_key"`
	LayoutBundleRevisionID   string                    `json:"layout_bundle_revision_id"`
	LayoutValidationID       string                    `json:"layout_validation_id"`
	ResolvedLayoutViewDigest string                    `json:"resolved_layout_view_digest"`
}

type activationVectorBundleKey struct {
	SlotOwnerKind      string `json:"slot_owner_kind"`
	SlotOwnerNamespace string `json:"slot_owner_namespace"`
	LocalComponentKey  string `json:"local_component_key"`
}

type activationEntryRecipe struct {
	EntryCount                       activationVectorDecimal `json:"entry_count"`
	SlotOwnerKind                    string                  `json:"slot_owner_kind"`
	SlotOwnerNamespace               string                  `json:"slot_owner_namespace"`
	LocalComponentKeyPrefix          string                  `json:"local_component_key_prefix"`
	LocalComponentKeyHexWidth        activationVectorDecimal `json:"local_component_key_hex_width"`
	LayoutBundleRevisionIDPrefix     string                  `json:"layout_bundle_revision_id_prefix"`
	LayoutBundleRevisionIDWidth      activationVectorDecimal `json:"layout_bundle_revision_id_width"`
	LayoutValidationIDPrefix         string                  `json:"layout_validation_id_prefix"`
	LayoutValidationIDWidth          activationVectorDecimal `json:"layout_validation_id_width"`
	ResolvedLayoutViewDigestPrefix   string                  `json:"resolved_layout_view_digest_prefix"`
	ResolvedLayoutViewDigestHexWidth activationVectorDecimal `json:"resolved_layout_view_digest_hex_width"`
}

type activationVectorDecimal struct {
	value   uint64
	present bool
}

func (value *activationVectorDecimal) UnmarshalJSON(data []byte) error {
	var text string
	if err := json.Unmarshal(data, &text); err != nil {
		return fmt.Errorf("canonical decimal must be a JSON string: %w", err)
	}
	if text == "" || (len(text) > 1 && text[0] == '0') {
		return fmt.Errorf("%q is not a canonical unsigned decimal", text)
	}
	for _, char := range []byte(text) {
		if char < '0' || char > '9' {
			return fmt.Errorf("%q is not a canonical unsigned decimal", text)
		}
	}
	parsed, err := strconv.ParseUint(text, 10, 64)
	if err != nil || strconv.FormatUint(parsed, 10) != text {
		return fmt.Errorf("%q is not a canonical uint64 decimal", text)
	}
	value.value = parsed
	value.present = true
	return nil
}

func (value activationVectorDecimal) required(label string) (uint64, error) {
	if !value.present {
		return 0, fmt.Errorf("%s must be present as a canonical decimal string", label)
	}
	return value.value, nil
}

func (value activationVectorSet) value() (Set, error) {
	set, err := value.activationVectorSetHeader.value()
	if err != nil {
		return Set{}, err
	}
	if value.Entries == nil {
		set.Entries = nil
		return set, nil
	}
	set.Entries = make([]Entry, len(value.Entries))
	for index, entry := range value.Entries {
		set.Entries[index] = entry.value()
	}
	return set, nil
}

func (value activationVectorSetHeader) value() (Set, error) {
	catalogRevision, err := value.CatalogSchema.CatalogSnapshot.CatalogRevision.required("catalog_schema.catalog_snapshot.catalog_revision")
	if err != nil {
		return Set{}, err
	}
	schemaGeneration, err := value.CatalogSchema.CatalogSnapshot.SchemaGeneration.required("catalog_schema.catalog_snapshot.schema_generation")
	if err != nil {
		return Set{}, err
	}
	engineGeneration, err := value.CatalogSchema.CatalogSnapshot.EngineGeneration.required("catalog_schema.catalog_snapshot.engine_generation")
	if err != nil {
		return Set{}, err
	}
	securityEpoch, err := value.RoutingSecurity.SecurityEpoch.required("routing_security.security_epoch")
	if err != nil {
		return Set{}, err
	}
	return Set{
		CanonicalizationVersion: value.CanonicalizationVersion,
		OrgID:                   value.OrgID,
		CatalogSchema: catalogpublicationv1.CatalogSchemaBinding{
			CatalogSnapshot: catalogpublicationv1.CatalogSnapshotBinding{
				OrgID:                 value.CatalogSchema.CatalogSnapshot.OrgID,
				CatalogRevision:       catalogRevision,
				CatalogSnapshotDigest: value.CatalogSchema.CatalogSnapshot.CatalogSnapshotDigest,
				SchemaGeneration:      schemaGeneration,
				EngineGeneration:      engineGeneration,
			},
			LayoutValidationViewDigest: value.CatalogSchema.LayoutValidationViewDigest,
		},
		RoutingSecurity: layoutroutingsecurityv1.Binding{
			OrgID:                           value.RoutingSecurity.OrgID,
			SecurityEpoch:                   securityEpoch,
			LayoutRoutingSecurityViewDigest: value.RoutingSecurity.LayoutRoutingSecurityViewDigest,
		},
		RequiredBundleKeySetDigest:  value.RequiredBundleKeySetDigest,
		PreparationProvenanceDigest: value.PreparationProvenanceDigest,
	}, nil
}

func (value activationVectorEntry) value() Entry {
	return Entry{
		BundleKey:                value.BundleKey.value(),
		LayoutBundleRevisionID:   LayoutBundleRevisionID(value.LayoutBundleRevisionID),
		LayoutValidationID:       LayoutValidationID(value.LayoutValidationID),
		ResolvedLayoutViewDigest: value.ResolvedLayoutViewDigest,
	}
}

func (value activationVectorBundleKey) value() layoutbundlekeyv1.BundleKey {
	return layoutbundlekeyv1.BundleKey{
		SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKind(value.SlotOwnerKind),
		SlotOwnerNamespace: value.SlotOwnerNamespace,
		LocalComponentKey:  layoutbundlekeyv1.LocalComponentKey(value.LocalComponentKey),
	}
}

func (vector activationCountBoundaryVector) value() (Set, error) {
	set, err := vector.LayoutActivationSet.value()
	if err != nil {
		return Set{}, err
	}
	entryCount, err := vector.EntryRecipe.EntryCount.required("entry_recipe.entry_count")
	if err != nil {
		return Set{}, err
	}
	keyWidth, err := vector.EntryRecipe.LocalComponentKeyHexWidth.required("entry_recipe.local_component_key_hex_width")
	if err != nil {
		return Set{}, err
	}
	revisionWidth, err := vector.EntryRecipe.LayoutBundleRevisionIDWidth.required("entry_recipe.layout_bundle_revision_id_width")
	if err != nil {
		return Set{}, err
	}
	validationWidth, err := vector.EntryRecipe.LayoutValidationIDWidth.required("entry_recipe.layout_validation_id_width")
	if err != nil {
		return Set{}, err
	}
	viewDigestWidth, err := vector.EntryRecipe.ResolvedLayoutViewDigestHexWidth.required("entry_recipe.resolved_layout_view_digest_hex_width")
	if err != nil {
		return Set{}, err
	}
	if entryCount == 0 || entryCount > MaxActivationEntries || keyWidth == 0 || keyWidth > 128 ||
		revisionWidth == 0 || revisionWidth > 32 || validationWidth == 0 || validationWidth > 32 ||
		viewDigestWidth == 0 || viewDigestWidth > 128 {
		return Set{}, fmt.Errorf("invalid count-boundary entry recipe")
	}

	set.Entries = make([]Entry, int(entryCount))
	for outputIndex, numericValue := 0, entryCount-1; ; outputIndex, numericValue = outputIndex+1, numericValue-1 {
		set.Entries[outputIndex] = Entry{
			BundleKey: layoutbundlekeyv1.BundleKey{
				SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKind(vector.EntryRecipe.SlotOwnerKind),
				SlotOwnerNamespace: vector.EntryRecipe.SlotOwnerNamespace,
				LocalComponentKey: layoutbundlekeyv1.LocalComponentKey(
					vector.EntryRecipe.LocalComponentKeyPrefix + fmt.Sprintf("%0*x", int(keyWidth), numericValue),
				),
			},
			LayoutBundleRevisionID: LayoutBundleRevisionID(
				vector.EntryRecipe.LayoutBundleRevisionIDPrefix + fmt.Sprintf("%0*d", int(revisionWidth), numericValue),
			),
			LayoutValidationID: LayoutValidationID(
				vector.EntryRecipe.LayoutValidationIDPrefix + fmt.Sprintf("%0*d", int(validationWidth), numericValue),
			),
			ResolvedLayoutViewDigest: vector.EntryRecipe.ResolvedLayoutViewDigestPrefix + fmt.Sprintf("%0*x", int(viewDigestWidth), numericValue),
		}
		if numericValue == 0 {
			break
		}
	}
	return set, nil
}

func assertActivationVectorIdentity(t *testing.T, set Set, canonicalHex, digest string) []byte {
	t.Helper()
	wantCanonical := mustDecodeActivationVectorHex(t, canonicalHex)
	canonical, err := CanonicalSetBytes(set)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(canonical, wantCanonical) {
		t.Fatalf("canonical bytes = %x, want %x", canonical, wantCanonical)
	}

	assertActivationVectorDigest(t, set, wantCanonical, digest)
	return canonical
}

func assertActivationVectorDigest(t *testing.T, set Set, canonical []byte, digest string) {
	t.Helper()
	sum := sha256.Sum256(canonical)
	if independentlyComputed := "sha256:" + hex.EncodeToString(sum[:]); independentlyComputed != digest {
		t.Fatalf("vector digest = %q, independently computed %q", digest, independentlyComputed)
	}
	computed, err := SetDigest(set)
	if err != nil {
		t.Fatal(err)
	}
	if computed != digest {
		t.Fatalf("SetDigest = %q, want %q", computed, digest)
	}
}

func mustDecodeActivationVectorHex(t *testing.T, value string) []byte {
	t.Helper()
	decoded, err := hex.DecodeString(value)
	if err != nil || value == "" || hex.EncodeToString(decoded) != value {
		t.Fatalf("invalid canonical lowercase hexadecimal %q: %v", value, err)
	}
	return decoded
}

func activationEntryCountOffset(set Set) int {
	return int(
		canonicalStringSize(set.CanonicalizationVersion) +
			canonicalStringSize(set.OrgID) +
			canonicalUintSize(set.CatalogSchema.CatalogSnapshot.CatalogRevision) +
			canonicalStringSize(set.CatalogSchema.CatalogSnapshot.CatalogSnapshotDigest) +
			canonicalUintSize(set.CatalogSchema.CatalogSnapshot.SchemaGeneration) +
			canonicalUintSize(set.CatalogSchema.CatalogSnapshot.EngineGeneration) +
			canonicalStringSize(set.CatalogSchema.LayoutValidationViewDigest) +
			canonicalUintSize(set.RoutingSecurity.SecurityEpoch) +
			canonicalStringSize(set.RoutingSecurity.LayoutRoutingSecurityViewDigest) +
			canonicalStringSize(layoutbundlekeyv1.RequiredBundleKeySetVersion) +
			canonicalStringSize(layoutbundlekeyv1.BundleKeyVersion) +
			canonicalStringSize(set.RequiredBundleKeySetDigest) +
			canonicalStringSize(set.PreparationProvenanceDigest),
	)
}

func cloneActivationSet(set Set) Set {
	clone := set
	if set.Entries != nil {
		clone.Entries = append([]Entry{}, set.Entries...)
	}
	return clone
}

func assertUniqueActivationVectorName(t *testing.T, seen map[string]bool, name string) {
	t.Helper()
	if name == "" || seen[name] {
		t.Fatalf("vector name %q is blank or duplicated", name)
	}
	seen[name] = true
}
