package layoutbundlekeyv1

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

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

func TestImmutableConformanceVectors(t *testing.T) {
	var document vectorDocument
	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-bundle-key.conformance/v1" {
		t.Fatalf("schema_version = %q", document.SchemaVersion)
	}
	if len(document.SetVectors) != 3 || len(document.CountBoundaryVectors) != 2 || len(document.RejectionVectors) != 2 {
		t.Fatalf("vector counts = %d/%d/%d", len(document.SetVectors), len(document.CountBoundaryVectors), len(document.RejectionVectors))
	}

	seen := map[string]bool{}
	for _, vector := range document.SetVectors {
		t.Run(vector.Name, func(t *testing.T) {
			testvectors.AssertUniqueVectorName(t, seen, vector.Name)
			set := vector.RequiredBundleKeySet.value()
			canonical, err := CanonicalRequiredBundleKeySetBytes(set)
			if err != nil {
				t.Fatal(err)
			}
			if got := hex.EncodeToString(canonical); got != vector.CanonicalBytesHex {
				t.Fatalf("canonical bytes = %s", got)
			}
			digest, err := RequiredBundleKeySetDigest(set)
			if err != nil {
				t.Fatal(err)
			}
			if digest != vector.RequiredBundleKeySetDigest {
				t.Fatalf("digest = %s", digest)
			}
		})
	}

	for _, vector := range document.CountBoundaryVectors {
		t.Run(vector.Name, func(t *testing.T) {
			testvectors.AssertUniqueVectorName(t, seen, vector.Name)
			if vector.DescendingLocalKeyStart < vector.DescendingLocalKeyEnd || vector.LocalKeyHexWidth != 64 {
				t.Fatal("invalid count-boundary fixture recipe")
			}
			keys := make([]BundleKey, 0, vector.DescendingLocalKeyStart-vector.DescendingLocalKeyEnd+1)
			for value := vector.DescendingLocalKeyStart; value >= vector.DescendingLocalKeyEnd; value-- {
				keys = append(keys, BundleKey{
					SlotOwnerKind:      SlotOwnerKind(vector.SlotOwnerKind),
					SlotOwnerNamespace: vector.SlotOwnerNamespace,
					LocalComponentKey:  LocalComponentKey(LocalComponentKeyPrefix + fmt.Sprintf("%0*x", vector.LocalKeyHexWidth, value)),
				})
			}
			set := RequiredBundleKeySet{Version: vector.Version, BundleKeys: keys}
			canonical, err := CanonicalRequiredBundleKeySetBytes(set)
			if err != nil {
				t.Fatal(err)
			}
			if len(canonical) != vector.CanonicalBytesLength {
				t.Fatalf("canonical length = %d", len(canonical))
			}
			countBytes, err := hex.DecodeString(vector.CanonicalCountBytesHex)
			if err != nil || hex.EncodeToString(countBytes) != vector.CanonicalCountBytesHex {
				t.Fatalf("invalid canonical_count_bytes_hex: %v", err)
			}
			countOffset := int(canonicalStringSize(vector.Version) + canonicalStringSize(BundleKeyVersion))
			if !bytes.Equal(canonical[countOffset:countOffset+len(countBytes)], countBytes) {
				t.Fatalf("count bytes = %x", canonical[countOffset:countOffset+len(countBytes)])
			}
			digest, err := RequiredBundleKeySetDigest(set)
			if err != nil {
				t.Fatal(err)
			}
			if digest != vector.RequiredBundleKeySetDigest {
				t.Fatalf("digest = %s", digest)
			}
		})
	}

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

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

type vectorDocument struct {
	SchemaVersion        string                `json:"schema_version"`
	SetVectors           []setVector           `json:"set_vectors"`
	CountBoundaryVectors []countBoundaryVector `json:"count_boundary_vectors"`
	RejectionVectors     []rejectionVector     `json:"rejection_vectors"`
}

type setVector struct {
	Name                       string            `json:"name"`
	RequiredBundleKeySet       vectorRequiredSet `json:"required_bundle_key_set"`
	CanonicalBytesHex          string            `json:"canonical_bytes_hex"`
	RequiredBundleKeySetDigest string            `json:"required_bundle_key_set_digest"`
}

type countBoundaryVector struct {
	Name                       string `json:"name"`
	Version                    string `json:"version"`
	SlotOwnerKind              string `json:"slot_owner_kind"`
	SlotOwnerNamespace         string `json:"slot_owner_namespace"`
	DescendingLocalKeyStart    int    `json:"descending_local_key_start"`
	DescendingLocalKeyEnd      int    `json:"descending_local_key_end"`
	LocalKeyHexWidth           int    `json:"local_key_hex_width"`
	CanonicalCountBytesHex     string `json:"canonical_count_bytes_hex"`
	CanonicalBytesLength       int    `json:"canonical_bytes_length"`
	RequiredBundleKeySetDigest string `json:"required_bundle_key_set_digest"`
}

type rejectionVector struct {
	Name                 string            `json:"name"`
	RequiredBundleKeySet vectorRequiredSet `json:"required_bundle_key_set"`
	ErrorContains        string            `json:"error_contains"`
}

type vectorRequiredSet struct {
	Version    string            `json:"version"`
	BundleKeys []vectorBundleKey `json:"bundle_keys"`
}

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

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