package applayoutbundleauthorityv1

import (
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"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"
)

func TestImmutableConformanceVectors(t *testing.T) {
	document := testvectors.DecodeVectorDocument[vectorDocument](t, ConformanceVectorsJSON())
	if document.SchemaVersion != "nexuscrm.layout-bundle-app-authority-evidence.conformance/v1" {
		t.Fatalf("schema_version = %q", document.SchemaVersion)
	}
	if len(document.EvidenceVectors) != 4 || len(document.ULEB128Vectors) != 7 || len(document.RejectionVectors) != 14 {
		t.Fatalf("vector counts = %d/%d/%d", len(document.EvidenceVectors), len(document.ULEB128Vectors), len(document.RejectionVectors))
	}

	evidenceByName := make(map[string]Evidence, len(document.EvidenceVectors))
	seenNames := map[string]bool{}
	for _, vector := range document.EvidenceVectors {
		t.Run(vector.Name, func(t *testing.T) {
			testvectors.AssertUniqueVectorName(t, seenNames, vector.Name)
			evidence, err := vector.Evidence.value()
			if err != nil {
				t.Fatal(err)
			}
			canonical, err := CanonicalEvidenceBytes(evidence)
			if err != nil {
				t.Fatal(err)
			}
			if got := hex.EncodeToString(canonical); got != vector.CanonicalBytesHex {
				t.Fatalf("canonical bytes = %s", got)
			}
			digestValue, err := EvidenceDigest(evidence)
			if err != nil {
				t.Fatal(err)
			}
			if digestValue != vector.EvidenceDigest {
				t.Fatalf("evidence digest = %s", digestValue)
			}
			evidenceByName[vector.Name] = evidence
		})
	}
	for _, required := range []string{"baseline", "utf8-source", "multibyte-uleb128-boundaries", "app-id-128-byte-boundary"} {
		if _, exists := evidenceByName[required]; !exists {
			t.Fatalf("required evidence vector %q is absent", required)
		}
	}
	if got := len(evidenceByName["app-id-128-byte-boundary"].BundleKey.SlotOwnerNamespace); got != 128 {
		t.Fatalf("AppID boundary vector length = %d, want 128", got)
	}

	for _, vector := range document.ULEB128Vectors {
		name := "uleb128-" + vector.ValueDecimal
		t.Run(name, func(t *testing.T) {
			testvectors.AssertUniqueVectorName(t, seenNames, name)
			value, err := strconv.ParseUint(vector.ValueDecimal, 10, 64)
			if err != nil || strconv.FormatUint(value, 10) != vector.ValueDecimal {
				t.Fatalf("non-canonical uint64 value %q: %v", vector.ValueDecimal, err)
			}
			var encoded [binary.MaxVarintLen64]byte
			count := binary.PutUvarint(encoded[:], value)
			if got := hex.EncodeToString(encoded[:count]); got != vector.EncodedHex {
				t.Fatalf("encoded bytes = %s", got)
			}
		})
	}

	for _, vector := range document.RejectionVectors {
		t.Run(vector.Name, func(t *testing.T) {
			testvectors.AssertUniqueVectorName(t, seenNames, vector.Name)
			base, exists := evidenceByName[vector.BaseEvidenceVector]
			if !exists {
				t.Fatalf("unknown base vector %q", vector.BaseEvidenceVector)
			}
			invalid := applyVectorMutation(t, base, vector)
			err := ValidateEvidence(invalid)
			if err == nil || !strings.Contains(err.Error(), vector.ErrorContains) {
				t.Fatalf("validation error = %v", err)
			}
		})
	}
	for _, required := range []string{
		"app-id-129-bytes",
		"app-id-64-byte-segment",
		"app-id-two-segments",
		"wrong-bundle-key-version",
		"uppercase-uuidv7",
		"invalid-uuid-variant",
	} {
		if !seenNames[required] {
			t.Fatalf("required rejection vector %q is absent", required)
		}
	}
}

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

func TestVectorUint64TextRejectsNonCanonicalAndOverflow(t *testing.T) {
	for _, raw := range []string{"", "00", "01", "+1", "-1", "18446744073709551616"} {
		t.Run(raw, func(t *testing.T) {
			if _, err := parseCanonicalVectorUint64("test", raw); err == nil {
				t.Fatalf("parseCanonicalVectorUint64(%q) succeeded", raw)
			}
		})
	}
	for _, raw := range []string{"0", "1", "18446744073709551615"} {
		t.Run("valid-"+raw, func(t *testing.T) {
			if _, err := parseCanonicalVectorUint64("test", raw); err != nil {
				t.Fatalf("parseCanonicalVectorUint64(%q): %v", raw, err)
			}
		})
	}
}

func applyVectorMutation(t *testing.T, value Evidence, vector rejectionVector) Evidence {
	t.Helper()
	if (vector.StringValue == nil) == (vector.Uint64Value == nil) {
		t.Fatal("rejection vector must specify exactly one replacement value")
	}
	switch vector.Field {
	case "EvidenceVersion":
		value.EvidenceVersion = requireStringMutation(t, vector)
	case "BundleKey.BundleKeyVersion":
		value.BundleKey.BundleKeyVersion = requireStringMutation(t, vector)
	case "SourceCatalogSnapshot.CatalogRevision":
		value.SourceCatalogSnapshot.CatalogRevision = requireUintMutation(t, vector)
	case "BundleKey.SlotOwnerKind":
		value.BundleKey.SlotOwnerKind = layoutbundlekeyv1.SlotOwnerKind(requireStringMutation(t, vector))
	case "BundleKey.SlotOwnerNamespace":
		value.BundleKey.SlotOwnerNamespace = requireStringMutation(t, vector)
	case "BundleKey.LocalComponentKey":
		value.BundleKey.LocalComponentKey = layoutbundlekeyv1.LocalComponentKey(requireStringMutation(t, vector))
	case "AppInstallationID":
		value.AppInstallationID = requireStringMutation(t, vector)
	case "AppReleaseDigest":
		value.AppReleaseDigest = requireStringMutation(t, vector)
	case "DefinitionVersion":
		value.DefinitionVersion = requireUintMutation(t, vector)
	default:
		t.Fatalf("unsupported mutation field %q", vector.Field)
	}
	return value
}

func requireStringMutation(t *testing.T, vector rejectionVector) string {
	t.Helper()
	if vector.StringValue == nil || vector.Uint64Value != nil {
		t.Fatal("mutation is not a string")
	}
	return *vector.StringValue
}

func requireUintMutation(t *testing.T, vector rejectionVector) uint64 {
	t.Helper()
	if vector.Uint64Value == nil || vector.StringValue != nil {
		t.Fatal("mutation is not a uint64")
	}
	value, err := parseCanonicalVectorUint64("mutation", *vector.Uint64Value)
	if err != nil {
		t.Fatal(err)
	}
	return value
}

func parseCanonicalVectorUint64(label, raw string) (uint64, error) {
	if raw == "" || (len(raw) > 1 && raw[0] == '0') {
		return 0, fmt.Errorf("%s uint64 %q is not canonical", label, raw)
	}
	value, err := strconv.ParseUint(raw, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("%s uint64 %q is invalid or overflows: %w", label, raw, err)
	}
	if strconv.FormatUint(value, 10) != raw {
		return 0, fmt.Errorf("%s uint64 %q is not canonical", label, raw)
	}
	return value, nil
}

type vectorDocument struct {
	SchemaVersion    string            `json:"schema_version"`
	EvidenceVectors  []evidenceVector  `json:"evidence_vectors"`
	ULEB128Vectors   []uleb128Vector   `json:"uleb128_vectors"`
	RejectionVectors []rejectionVector `json:"rejection_vectors"`
}

type evidenceVector struct {
	Name              string         `json:"name"`
	Evidence          vectorEvidence `json:"evidence"`
	CanonicalBytesHex string         `json:"canonical_bytes_hex"`
	EvidenceDigest    string         `json:"evidence_digest"`
}

type uleb128Vector struct {
	ValueDecimal string `json:"value_decimal"`
	EncodedHex   string `json:"encoded_hex"`
}

type rejectionVector struct {
	Name               string  `json:"name"`
	BaseEvidenceVector string  `json:"base_evidence_vector"`
	Field              string  `json:"field"`
	StringValue        *string `json:"string_value,omitempty"`
	Uint64Value        *string `json:"uint64_value,omitempty"`
	ErrorContains      string  `json:"error_contains"`
}

type vectorEvidence struct {
	EvidenceVersion                string                       `json:"evidence_version"`
	SourceCatalogSnapshot          vectorCatalogSnapshotBinding `json:"source_catalog_snapshot"`
	BundleKey                      vectorBundleKey              `json:"bundle_key"`
	AppInstallationID              string                       `json:"app_installation_id"`
	AppReleaseDigest               string                       `json:"app_release_digest"`
	DeclarationComponentID         string                       `json:"declaration_component_id"`
	DefinitionVersion              string                       `json:"definition_version"`
	LayoutBundleSlotRevisionDigest string                       `json:"layout_bundle_slot_revision_digest"`
}

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

type vectorBundleKey struct {
	BundleKeyVersion   string `json:"bundle_key_version"`
	SlotOwnerKind      string `json:"slot_owner_kind"`
	SlotOwnerNamespace string `json:"slot_owner_namespace"`
	LocalComponentKey  string `json:"local_component_key"`
}

func (value vectorEvidence) value() (Evidence, error) {
	catalogRevision, err := parseCanonicalVectorUint64("catalog_revision", value.SourceCatalogSnapshot.CatalogRevision)
	if err != nil {
		return Evidence{}, err
	}
	schemaGeneration, err := parseCanonicalVectorUint64("schema_generation", value.SourceCatalogSnapshot.SchemaGeneration)
	if err != nil {
		return Evidence{}, err
	}
	engineGeneration, err := parseCanonicalVectorUint64("engine_generation", value.SourceCatalogSnapshot.EngineGeneration)
	if err != nil {
		return Evidence{}, err
	}
	definitionVersion, err := parseCanonicalVectorUint64("definition_version", value.DefinitionVersion)
	if err != nil {
		return Evidence{}, err
	}
	return Evidence{
		EvidenceVersion: value.EvidenceVersion,
		SourceCatalogSnapshot: catalogpublicationv1.CatalogSnapshotBinding{
			OrgID:                 value.SourceCatalogSnapshot.OrgID,
			CatalogRevision:       catalogRevision,
			CatalogSnapshotDigest: value.SourceCatalogSnapshot.CatalogSnapshotDigest,
			SchemaGeneration:      schemaGeneration,
			EngineGeneration:      engineGeneration,
		},
		BundleKey: BundleKey{
			BundleKeyVersion:   value.BundleKey.BundleKeyVersion,
			SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKind(value.BundleKey.SlotOwnerKind),
			SlotOwnerNamespace: value.BundleKey.SlotOwnerNamespace,
			LocalComponentKey:  layoutbundlekeyv1.LocalComponentKey(value.BundleKey.LocalComponentKey),
		},
		AppInstallationID:              value.AppInstallationID,
		AppReleaseDigest:               value.AppReleaseDigest,
		DeclarationComponentID:         value.DeclarationComponentID,
		DefinitionVersion:              definitionVersion,
		LayoutBundleSlotRevisionDigest: value.LayoutBundleSlotRevisionDigest,
	}, nil
}
