package catalogpublication

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"io"
	"reflect"
	"strings"
	"testing"

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

func TestSourceAppSelectionSetImmutableConformanceVectors(t *testing.T) {
	var document sourceAppSelectionVectorDocument
	decoder := json.NewDecoder(bytes.NewReader(SourceAppSelectionSetConformanceVectorsJSON()))
	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.catalog-source-app-selection-set.conformance/v1" {
		t.Fatalf("schema_version = %q", document.SchemaVersion)
	}
	if len(document.AcceptedVectors) != 5 || len(document.RejectionVectors) != 8 || len(document.BoundaryVectors) != 4 {
		t.Fatalf(
			"vector counts = %d/%d/%d",
			len(document.AcceptedVectors),
			len(document.RejectionVectors),
			len(document.BoundaryVectors),
		)
	}

	seen := map[string]bool{}
	for _, vector := range document.AcceptedVectors {
		t.Run(vector.Name, func(t *testing.T) {
			assertUniqueSourceAppSelectionVectorName(t, seen, vector.Name)
			set := vector.SourceAppSelectionSet.value()
			requiredSet := vector.RequiredBundleKeySet.value()
			originalEntries := append([]SourceAppSelection{}, set.Entries...)

			canonical, err := CanonicalizeSourceAppSelectionSet(set, vector.WriterCommandID, requiredSet)
			if err != nil {
				t.Fatal(err)
			}
			if !reflect.DeepEqual(set.Entries, originalEntries) {
				t.Fatal("canonicalization mutated input entries")
			}
			appIDs := make([]string, len(canonical.Entries))
			for index, entry := range canonical.Entries {
				appIDs[index] = entry.AppID
			}
			if !reflect.DeepEqual(appIDs, vector.CanonicalAppIDs) {
				t.Fatalf("canonical AppIDs = %#v", appIDs)
			}
			canonicalBytes, err := CanonicalSourceAppSelectionSetBytes(set, vector.WriterCommandID, requiredSet)
			if err != nil {
				t.Fatal(err)
			}
			if got := hex.EncodeToString(canonicalBytes); got != vector.CanonicalBytesHex {
				t.Fatalf("canonical bytes = %s", got)
			}
		})
	}

	for _, vector := range document.RejectionVectors {
		t.Run(vector.Name, func(t *testing.T) {
			assertUniqueSourceAppSelectionVectorName(t, seen, vector.Name)
			err := ValidateSourceAppSelectionSet(
				vector.SourceAppSelectionSet.value(),
				vector.WriterCommandID,
				vector.RequiredBundleKeySet.value(),
			)
			if err == nil || !strings.Contains(err.Error(), vector.ErrorContains) {
				t.Fatalf("validation error = %v", err)
			}
		})
	}

	for _, vector := range document.BoundaryVectors {
		t.Run(vector.Name, func(t *testing.T) {
			assertUniqueSourceAppSelectionVectorName(t, seen, vector.Name)
			var err error
			switch vector.Kind {
			case "entry_count":
				err = validateSourceAppSelectionEntryCount(vector.Value)
			case "canonical_bytes":
				err = validateSourceAppSelectionCanonicalSize(vector.Value)
			default:
				t.Fatalf("unknown boundary kind %q", vector.Kind)
			}
			if vector.Accepted {
				if err != nil {
					t.Fatalf("boundary rejected: %v", err)
				}
				return
			}
			if err == nil || !strings.Contains(err.Error(), vector.ErrorContains) {
				t.Fatalf("boundary error = %v", err)
			}
		})
	}
}

func TestSourceAppSelectionSetCanonicalizationIsDefensive(t *testing.T) {
	writerCommandID := "018f0000-0000-7000-9000-000000000001"
	set := SourceAppSelectionSet{
		SourceAppSelectionSetVersion: SourceAppSelectionSetVersion,
		Entries: []SourceAppSelection{
			validSourceAppSelection("io.example.zeta", "018f0000-0000-7000-9000-000000000012"),
			validSourceAppSelection("io.example.alpha", "018f0000-0000-7000-9000-000000000011"),
		},
	}
	requiredSet := emptyRequiredBundleKeySet()

	canonical, err := CanonicalizeSourceAppSelectionSet(set, writerCommandID, requiredSet)
	if err != nil {
		t.Fatal(err)
	}
	if canonical.Entries == nil || canonical.Entries[0].AppID != "io.example.alpha" {
		t.Fatalf("canonical entries = %#v", canonical.Entries)
	}
	set.Entries[1].AppID = "io.example.changed"
	if canonical.Entries[0].AppID != "io.example.alpha" {
		t.Fatal("canonical entries alias caller storage")
	}
	canonical.Entries[0].AppID = "io.example.mutated"
	if set.Entries[0].AppID != "io.example.zeta" {
		t.Fatal("caller entries alias canonical storage")
	}

	first, err := CanonicalSourceAppSelectionSetBytes(
		SourceAppSelectionSet{
			SourceAppSelectionSetVersion: SourceAppSelectionSetVersion,
			Entries: []SourceAppSelection{
				validSourceAppSelection("io.example.alpha", "018f0000-0000-7000-9000-000000000011"),
			},
		},
		writerCommandID,
		requiredSet,
	)
	if err != nil {
		t.Fatal(err)
	}
	first[0] ^= 0xff
	second, err := CanonicalSourceAppSelectionSetBytes(
		SourceAppSelectionSet{
			SourceAppSelectionSetVersion: SourceAppSelectionSetVersion,
			Entries: []SourceAppSelection{
				validSourceAppSelection("io.example.alpha", "018f0000-0000-7000-9000-000000000011"),
			},
		},
		writerCommandID,
		requiredSet,
	)
	if err != nil {
		t.Fatal(err)
	}
	if bytes.Equal(first, second) {
		t.Fatal("canonical bytes reused caller-mutable storage")
	}
}

func TestSourceAppSelectionSetValidationUsesExactFieldGrammars(t *testing.T) {
	writerCommandID := "018f0000-0000-7000-9000-000000000001"
	tests := map[string]func(*SourceAppSelectionSet, *string, *layoutbundlekeyv1.RequiredBundleKeySet){
		"version": func(set *SourceAppSelectionSet, _ *string, _ *layoutbundlekeyv1.RequiredBundleKeySet) {
			set.SourceAppSelectionSetVersion = "nexuscrm.catalog-source-app-selection-set/v2"
		},
		"writer command": func(_ *SourceAppSelectionSet, writer *string, _ *layoutbundlekeyv1.RequiredBundleKeySet) {
			*writer = "018F0000-0000-7000-9000-000000000001"
		},
		"AppID": func(set *SourceAppSelectionSet, _ *string, _ *layoutbundlekeyv1.RequiredBundleKeySet) {
			set.Entries[0].AppID = "io.example"
		},
		"AppInstallationID": func(set *SourceAppSelectionSet, _ *string, _ *layoutbundlekeyv1.RequiredBundleKeySet) {
			set.Entries[0].AppInstallationID = "018f0000-0000-4000-9000-000000000011"
		},
		"AppReleaseDigest": func(set *SourceAppSelectionSet, _ *string, _ *layoutbundlekeyv1.RequiredBundleKeySet) {
			set.Entries[0].AppReleaseDigest = "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
		},
		"SelectionKind": func(set *SourceAppSelectionSet, _ *string, _ *layoutbundlekeyv1.RequiredBundleKeySet) {
			set.Entries[0].SelectionKind = "PREFERRED"
		},
		"RequiredBundleKeySet": func(_ *SourceAppSelectionSet, _ *string, required *layoutbundlekeyv1.RequiredBundleKeySet) {
			required.BundleKeys = nil
		},
	}
	for name, mutate := range tests {
		t.Run(name, func(t *testing.T) {
			set := SourceAppSelectionSet{
				SourceAppSelectionSetVersion: SourceAppSelectionSetVersion,
				Entries: []SourceAppSelection{
					validSourceAppSelection("io.example.alpha", "018f0000-0000-7000-9000-000000000011"),
				},
			}
			writer := writerCommandID
			requiredSet := emptyRequiredBundleKeySet()
			mutate(&set, &writer, &requiredSet)
			if err := ValidateSourceAppSelectionSet(set, writer, requiredSet); err == nil {
				t.Fatal("invalid value accepted")
			}
		})
	}
}

func TestSourceAppSelectionSetCountLimitRunsBeforeEntryValidation(t *testing.T) {
	set := SourceAppSelectionSet{
		SourceAppSelectionSetVersion: SourceAppSelectionSetVersion,
		Entries:                      make([]SourceAppSelection, MaxSourceAppSelectionEntries+1),
	}
	err := ValidateSourceAppSelectionSet(
		set,
		"018f0000-0000-7000-9000-000000000001",
		emptyRequiredBundleKeySet(),
	)
	if err == nil || !strings.Contains(err.Error(), "exceeds 100000 entries") {
		t.Fatalf("validation error = %v", err)
	}
}

func TestSourceAppSelectionSetConformanceVectorsJSONReturnsDefensiveCopy(t *testing.T) {
	copyData := SourceAppSelectionSetConformanceVectorsJSON()
	if len(copyData) == 0 {
		t.Fatal("embedded vectors are empty")
	}
	copyData[0] ^= 0xff
	if bytes.Equal(copyData, SourceAppSelectionSetConformanceVectorsJSON()) {
		t.Fatal("SourceAppSelectionSetConformanceVectorsJSON returned shared mutable storage")
	}
}

type sourceAppSelectionVectorDocument struct {
	SchemaVersion    string                              `json:"schema_version"`
	AcceptedVectors  []sourceAppSelectionAcceptedVector  `json:"accepted_vectors"`
	RejectionVectors []sourceAppSelectionRejectionVector `json:"rejection_vectors"`
	BoundaryVectors  []sourceAppSelectionBoundaryVector  `json:"boundary_vectors"`
}

type sourceAppSelectionAcceptedVector struct {
	Name                  string                              `json:"name"`
	WriterCommandID       string                              `json:"writer_command_id"`
	RequiredBundleKeySet  sourceAppSelectionVectorRequiredSet `json:"required_bundle_key_set"`
	SourceAppSelectionSet sourceAppSelectionVectorSet         `json:"source_app_selection_set"`
	CanonicalAppIDs       []string                            `json:"canonical_app_ids"`
	CanonicalBytesHex     string                              `json:"canonical_bytes_hex"`
}

type sourceAppSelectionRejectionVector struct {
	Name                  string                              `json:"name"`
	WriterCommandID       string                              `json:"writer_command_id"`
	RequiredBundleKeySet  sourceAppSelectionVectorRequiredSet `json:"required_bundle_key_set"`
	SourceAppSelectionSet sourceAppSelectionVectorSet         `json:"source_app_selection_set"`
	ErrorContains         string                              `json:"error_contains"`
}

type sourceAppSelectionBoundaryVector struct {
	Name          string `json:"name"`
	Kind          string `json:"kind"`
	Value         uint64 `json:"value"`
	Accepted      bool   `json:"accepted"`
	ErrorContains string `json:"error_contains"`
}

type sourceAppSelectionVectorRequiredSet struct {
	Version    string                              `json:"version"`
	BundleKeys []sourceAppSelectionVectorBundleKey `json:"bundle_keys"`
}

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

type sourceAppSelectionVectorSet struct {
	Version string                          `json:"version"`
	Entries []sourceAppSelectionVectorEntry `json:"entries"`
}

type sourceAppSelectionVectorEntry struct {
	AppID                          string `json:"app_id"`
	AppInstallationID              string `json:"app_installation_id"`
	AppReleaseDigest               string `json:"app_release_digest"`
	SelectionKind                  string `json:"selection_kind"`
	AppLifecycleCandidateCommandID string `json:"app_lifecycle_candidate_command_id"`
}

func (value sourceAppSelectionVectorRequiredSet) value() layoutbundlekeyv1.RequiredBundleKeySet {
	keys := make([]layoutbundlekeyv1.BundleKey, len(value.BundleKeys))
	for index, key := range value.BundleKeys {
		keys[index] = layoutbundlekeyv1.BundleKey{
			SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKind(key.SlotOwnerKind),
			SlotOwnerNamespace: key.SlotOwnerNamespace,
			LocalComponentKey:  layoutbundlekeyv1.LocalComponentKey(key.LocalComponentKey),
		}
	}
	return layoutbundlekeyv1.RequiredBundleKeySet{
		Version:    value.Version,
		BundleKeys: keys,
	}
}

func (value sourceAppSelectionVectorSet) value() SourceAppSelectionSet {
	var entries []SourceAppSelection
	if value.Entries != nil {
		entries = make([]SourceAppSelection, len(value.Entries))
		for index, entry := range value.Entries {
			entries[index] = SourceAppSelection{
				AppID:                          entry.AppID,
				AppInstallationID:              entry.AppInstallationID,
				AppReleaseDigest:               entry.AppReleaseDigest,
				SelectionKind:                  SourceAppSelectionKind(entry.SelectionKind),
				AppLifecycleCandidateCommandID: entry.AppLifecycleCandidateCommandID,
			}
		}
	}
	return SourceAppSelectionSet{
		SourceAppSelectionSetVersion: value.Version,
		Entries:                      entries,
	}
}

func assertUniqueSourceAppSelectionVectorName(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
}

func validSourceAppSelection(appID, installationID string) SourceAppSelection {
	return SourceAppSelection{
		AppID:             appID,
		AppInstallationID: installationID,
		AppReleaseDigest:  "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
		SelectionKind:     SourceAppSelectionKindActive,
	}
}

func emptyRequiredBundleKeySet() layoutbundlekeyv1.RequiredBundleKeySet {
	return layoutbundlekeyv1.RequiredBundleKeySet{
		Version:    layoutbundlekeyv1.RequiredBundleKeySetVersion,
		BundleKeys: []layoutbundlekeyv1.BundleKey{},
	}
}
