package catalogassignmentidentityv1

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"strconv"
	"testing"

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

func TestConformanceVectors(t *testing.T) {
	var document identityVectorDocument
	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.catalog-assignment-identity-view-conformance/v1" {
		t.Fatalf("schema = %q", document.SchemaVersion)
	}

	normalizationNames := map[string]bool{}
	for _, vector := range document.NormalizationVectors {
		if vector.Name == "" || normalizationNames[vector.Name] {
			t.Fatalf("normalization vector name %q is blank or duplicated", vector.Name)
		}
		normalizationNames[vector.Name] = true
		t.Run("normalize/"+vector.Name, func(t *testing.T) {
			key, err := NormalizeObjectAPIName(vector.SourceAPIName)
			if err != nil {
				t.Fatal(err)
			}
			if key.NormalizedAPIName != vector.NormalizedAPIName {
				t.Fatalf("normalized API name = %q, want %q", key.NormalizedAPIName, vector.NormalizedAPIName)
			}
		})
	}
	for _, required := range []string{"custom-object-uppercase-suffix", "leading-underscore-and-uppercase"} {
		if !normalizationNames[required] {
			t.Fatalf("required normalization vector %q missing", required)
		}
	}

	viewNames := map[string]bool{}
	productAppTargetCases := map[string]bool{}
	for _, vector := range document.ViewVectors {
		if vector.Name == "" || viewNames[vector.Name] {
			t.Fatalf("view vector name %q is blank or duplicated", vector.Name)
		}
		viewNames[vector.Name] = true
		for _, identity := range vector.View.ProductApps {
			productAppTargetCases[string(identity.State)+"/"+identity.ResolvedTarget.Kind] = true
		}
		t.Run("view/"+vector.Name, func(t *testing.T) {
			assertIdentityVectorResult(t, vector.View.view(t), vector.CanonicalBytesLength, vector.CanonicalBytesHex, vector.ViewDigest)
		})
	}
	for _, required := range []string{
		"all-dimensions-permuted",
		"unsigned-leb128-lower-boundaries",
		"unsigned-leb128-upper-boundaries",
	} {
		if !viewNames[required] {
			t.Fatalf("required view vector %q missing", required)
		}
	}
	for _, required := range []string{
		"ACTIVE/APP_INSTALLATION_ID",
		"TOMBSTONE/APP_INSTALLATION_ID",
		"TOMBSTONE/ABSENT",
	} {
		if !productAppTargetCases[required] {
			t.Fatalf("required Product-App target vector %q missing", required)
		}
	}

	recipeNames := map[string]bool{}
	for _, vector := range document.RecipeVectors {
		if vector.Name == "" || recipeNames[vector.Name] {
			t.Fatalf("recipe vector name %q is blank or duplicated", vector.Name)
		}
		recipeNames[vector.Name] = true
		t.Run("recipe/"+vector.Name, func(t *testing.T) {
			assertIdentityVectorResult(t, vector.view(t), vector.CanonicalBytesLength, "", vector.ViewDigest)
		})
	}
	for _, required := range []string{"object-count-127", "object-count-128"} {
		if !recipeNames[required] {
			t.Fatalf("required recipe vector %q missing", required)
		}
	}
}

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

func assertIdentityVectorResult(t *testing.T, view View, rawLength, wantHex, wantDigest string) {
	t.Helper()
	canonical, err := CanonicalViewBytes(view)
	if err != nil {
		t.Fatal(err)
	}
	wantLength := identityVectorUint(t, "canonical_bytes_length", rawLength)
	if uint64(len(canonical)) != wantLength {
		t.Fatalf("canonical bytes length = %d, want %d; hex=%s", len(canonical), wantLength, hex.EncodeToString(canonical))
	}
	if wantHex != "" {
		decoded, err := hex.DecodeString(wantHex)
		if err != nil {
			t.Fatalf("canonical_bytes_hex is invalid: %v", err)
		}
		if !bytes.Equal(canonical, decoded) {
			t.Fatalf("canonical bytes = %s, want %s", hex.EncodeToString(canonical), wantHex)
		}
	}
	digest, err := ViewDigest(view)
	if err != nil {
		t.Fatal(err)
	}
	if digest != wantDigest {
		t.Fatalf("view digest = %s, want %s", digest, wantDigest)
	}
}

type identityVectorDocument struct {
	SchemaVersion        string                        `json:"schema_version"`
	NormalizationVectors []identityNormalizationVector `json:"normalization_vectors"`
	ViewVectors          []identityViewVector          `json:"view_vectors"`
	RecipeVectors        []identityRecipeVector        `json:"recipe_vectors"`
}

type identityNormalizationVector struct {
	Name              string `json:"name"`
	SourceAPIName     string `json:"source_api_name"`
	NormalizedAPIName string `json:"normalized_api_name"`
}

type identityViewVector struct {
	Name                 string             `json:"name"`
	View                 identityVectorView `json:"view"`
	CanonicalBytesLength string             `json:"canonical_bytes_length"`
	CanonicalBytesHex    string             `json:"canonical_bytes_hex"`
	ViewDigest           string             `json:"view_digest"`
}

type identityVectorView struct {
	SourceCatalogSnapshot         identityVectorSnapshot        `json:"source_catalog_snapshot"`
	CanonicalizationVersion       string                        `json:"canonicalization_version"`
	KeyGrammarVersion             string                        `json:"key_grammar_version"`
	ObjectKeyNormalizationVersion string                        `json:"object_key_normalization_version"`
	Objects                       []identityVectorObject        `json:"objects"`
	RecordTypes                   []identityVectorRecordType    `json:"record_types"`
	ProductApps                   []identityVectorProductApp    `json:"product_apps"`
	NavigationApps                []identityVectorNavigationApp `json:"navigation_apps"`
}

type identityVectorSnapshot 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 identityVectorObject struct {
	NormalizedAPIName string        `json:"normalized_api_name"`
	ResolvedObjectID  string        `json:"resolved_object_id"`
	State             IdentityState `json:"state"`
}

type identityVectorRecordType struct {
	ObjectNormalizedAPIName string        `json:"object_normalized_api_name"`
	NamespaceID             string        `json:"namespace_id"`
	LocalKey                string        `json:"local_key"`
	ResolvedRecordTypeID    string        `json:"resolved_record_type_id"`
	State                   IdentityState `json:"state"`
}

type identityVectorProductApp struct {
	AppID          string                                 `json:"app_id"`
	State          IdentityState                          `json:"state"`
	ResolvedTarget identityVectorProductAppResolvedTarget `json:"resolved_target"`
}

type identityVectorProductAppResolvedTarget struct {
	Kind                      string  `json:"kind"`
	ResolvedAppInstallationID *string `json:"resolved_app_installation_id"`
}

type identityVectorNavigationApp struct {
	NamespaceID         string        `json:"namespace_id"`
	LocalKey            string        `json:"local_key"`
	ResolvedAppConfigID string        `json:"resolved_app_config_id"`
	State               IdentityState `json:"state"`
}

func (value identityVectorView) view(t *testing.T) View {
	t.Helper()
	view := View{
		SourceCatalogSnapshot:         value.SourceCatalogSnapshot.binding(t),
		CanonicalizationVersion:       value.CanonicalizationVersion,
		KeyGrammarVersion:             value.KeyGrammarVersion,
		ObjectKeyNormalizationVersion: value.ObjectKeyNormalizationVersion,
		Objects:                       make([]ObjectIdentity, len(value.Objects)),
		RecordTypes:                   make([]RecordTypeIdentity, len(value.RecordTypes)),
		ProductApps:                   make([]ProductAppIdentity, len(value.ProductApps)),
		NavigationApps:                make([]NavigationAppIdentity, len(value.NavigationApps)),
	}
	for index, identity := range value.Objects {
		view.Objects[index] = ObjectIdentity{
			Key:              ObjectKey{NormalizedAPIName: identity.NormalizedAPIName},
			ResolvedObjectID: identity.ResolvedObjectID,
			State:            identity.State,
		}
	}
	for index, identity := range value.RecordTypes {
		view.RecordTypes[index] = RecordTypeIdentity{
			ObjectKey:            ObjectKey{NormalizedAPIName: identity.ObjectNormalizedAPIName},
			Key:                  RecordTypeKey{NamespaceID: identity.NamespaceID, LocalKey: identity.LocalKey},
			ResolvedRecordTypeID: identity.ResolvedRecordTypeID,
			State:                identity.State,
		}
	}
	for index, identity := range value.ProductApps {
		view.ProductApps[index] = ProductAppIdentity{
			Key:            ProductAppKey{AppID: identity.AppID},
			State:          identity.State,
			ResolvedTarget: identity.ResolvedTarget.target(t),
		}
	}
	for index, identity := range value.NavigationApps {
		view.NavigationApps[index] = NavigationAppIdentity{
			Key:                 NavigationAppKey{NamespaceID: identity.NamespaceID, LocalKey: identity.LocalKey},
			ResolvedAppConfigID: identity.ResolvedAppConfigID,
			State:               identity.State,
		}
	}
	return view
}

func (value identityVectorProductAppResolvedTarget) target(t *testing.T) ProductAppResolvedTarget {
	t.Helper()
	switch value.Kind {
	case "ABSENT":
		if value.ResolvedAppInstallationID != nil {
			t.Fatal("ABSENT vector target must omit resolved_app_installation_id")
		}
		return ProductAppResolvedTarget{Kind: ProductAppResolvedTargetKindAbsent}
	case "APP_INSTALLATION_ID":
		if value.ResolvedAppInstallationID == nil {
			t.Fatal("APP_INSTALLATION_ID vector target must include resolved_app_installation_id")
		}
		return ProductAppResolvedTarget{
			Kind:                      ProductAppResolvedTargetKindAppInstallationID,
			ResolvedAppInstallationID: *value.ResolvedAppInstallationID,
		}
	default:
		t.Fatalf("unknown Product-App target kind %q", value.Kind)
		return ProductAppResolvedTarget{}
	}
}

func (value identityVectorSnapshot) binding(t *testing.T) catalogpublicationv1.CatalogSnapshotBinding {
	t.Helper()
	return catalogpublicationv1.CatalogSnapshotBinding{
		OrgID:                 value.OrgID,
		CatalogRevision:       identityVectorUint(t, "catalog_revision", value.CatalogRevision),
		CatalogSnapshotDigest: value.CatalogSnapshotDigest,
		SchemaGeneration:      identityVectorUint(t, "schema_generation", value.SchemaGeneration),
		EngineGeneration:      identityVectorUint(t, "engine_generation", value.EngineGeneration),
	}
}

type identityRecipeVector struct {
	Name                          string                 `json:"name"`
	SourceCatalogSnapshot         identityVectorSnapshot `json:"source_catalog_snapshot"`
	CanonicalizationVersion       string                 `json:"canonicalization_version"`
	KeyGrammarVersion             string                 `json:"key_grammar_version"`
	ObjectKeyNormalizationVersion string                 `json:"object_key_normalization_version"`
	Objects                       identityObjectRecipe   `json:"objects"`
	RecordTypes                   []struct{}             `json:"record_types"`
	ProductApps                   []struct{}             `json:"product_apps"`
	NavigationApps                []struct{}             `json:"navigation_apps"`
	CanonicalBytesLength          string                 `json:"canonical_bytes_length"`
	ViewDigest                    string                 `json:"view_digest"`
}

type identityObjectRecipe struct {
	Count                   string        `json:"count"`
	IndexStart              string        `json:"index_start"`
	IndexEndInclusive       string        `json:"index_end_inclusive"`
	InputOrder              string        `json:"input_order"`
	NormalizedAPINameFormat string        `json:"normalized_api_name_format"`
	ResolvedObjectIDFormat  string        `json:"resolved_object_id_format"`
	State                   IdentityState `json:"state"`
}

func (value identityRecipeVector) view(t *testing.T) View {
	t.Helper()
	if len(value.RecordTypes) != 0 || len(value.ProductApps) != 0 || len(value.NavigationApps) != 0 {
		t.Fatal("v1 object-count recipe requires explicit empty non-object dimensions")
	}
	count := identityVectorUint(t, "objects.count", value.Objects.Count)
	start := identityVectorUint(t, "objects.index_start", value.Objects.IndexStart)
	end := identityVectorUint(t, "objects.index_end_inclusive", value.Objects.IndexEndInclusive)
	if count == 0 || start != 0 || end+1 != count || value.Objects.InputOrder != "descending" ||
		value.Objects.NormalizedAPINameFormat != "o%03d" || value.Objects.ResolvedObjectIDFormat != "id-%03d" {
		t.Fatal("unsupported or internally inconsistent object-count recipe")
	}
	view := View{
		SourceCatalogSnapshot:         value.SourceCatalogSnapshot.binding(t),
		CanonicalizationVersion:       value.CanonicalizationVersion,
		KeyGrammarVersion:             value.KeyGrammarVersion,
		ObjectKeyNormalizationVersion: value.ObjectKeyNormalizationVersion,
		Objects:                       make([]ObjectIdentity, 0, int(count)),
		RecordTypes:                   []RecordTypeIdentity{},
		ProductApps:                   []ProductAppIdentity{},
		NavigationApps:                []NavigationAppIdentity{},
	}
	for index := int(end); index >= int(start); index-- {
		view.Objects = append(view.Objects, ObjectIdentity{
			Key:              ObjectKey{NormalizedAPIName: fmt.Sprintf("o%03d", index)},
			ResolvedObjectID: fmt.Sprintf("id-%03d", index),
			State:            value.Objects.State,
		})
	}
	return view
}

func identityVectorUint(t *testing.T, label, raw string) uint64 {
	t.Helper()
	parsed, err := strconv.ParseUint(raw, 10, 64)
	if err != nil || raw != strconv.FormatUint(parsed, 10) {
		t.Fatalf("%s = %q is not a canonical uint64: %v", label, raw, err)
	}
	return parsed
}
