package layoutbundleslotv1

import (
	"bytes"
	"sort"
	"strings"
	"testing"

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

func TestVerifyPackageDeclarationsInitialAndSuccessor(t *testing.T) {
	initialFixture, initial := fixtureDeclaration(t, 1, "Issue", nil)
	if initial.AppID != fixtureAppID || initial.NamespaceID != fixtureNamespace || initial.LocalComponentKey != fixtureLocalKey {
		t.Fatalf("initial declaration identity = %#v", initial)
	}
	if initial.DeclarationComponentID != initialFixture.pkg.Bundle.Components[initialFixture.slotIndex].ComponentID {
		t.Fatalf("declaration ComponentID = %s", initial.DeclarationComponentID)
	}
	if initial.PreviousTargetComponentID != nil {
		t.Fatal("initial declaration retained a predecessor")
	}
	_, successor := fixtureDeclaration(t, 2, "IssueV2", &initial.TargetComponentID)
	if successor.PreviousTargetComponentID == nil || *successor.PreviousTargetComponentID != initial.TargetComponentID {
		t.Fatalf("successor declaration = %#v", successor)
	}
	if successor.DeclarationComponentID != initial.DeclarationComponentID {
		t.Fatal("stable declaration ComponentID changed across versions")
	}
}

func TestVerifyPackageDeclarationsAllowsNoSlots(t *testing.T) {
	fixture := newSlotPackageFixture(t, 1, "Issue", nil)
	fixture.pkg.Bundle.Components = append([]appauthority.Component(nil), fixture.pkg.Bundle.Components[1-fixture.slotIndex])
	fixture.pkg.ComponentBlobs = append([]appauthority.ComponentBlob(nil), fixture.pkg.ComponentBlobs[1-fixture.slotIndex])
	rehashFixture(t, &fixture.pkg)
	_, declarations, err := VerifyPackageDeclarations(fixture.pkg)
	if err != nil {
		t.Fatal(err)
	}
	if declarations == nil || len(declarations) != 0 {
		t.Fatalf("declarations = %#v", declarations)
	}
}

func TestVerifyPackageDeclarationsRejectsDescriptorAndBehaviorMismatch(t *testing.T) {
	for name, mutate := range map[string]func(*testing.T, *slotPackageFixture){
		"format": func(t *testing.T, fixture *slotPackageFixture) {
			fixture.pkg.Bundle.Components[fixture.slotIndex].ComponentFormatVersion = 2
			rehashFixture(t, &fixture.pkg)
		},
		"override policy": func(t *testing.T, fixture *slotPackageFixture) {
			fixture.pkg.Bundle.Components[fixture.slotIndex].OverridePolicy = appauthority.OverridePolicyPreserveAuthorized
			rehashFixture(t, &fixture.pkg)
		},
		"removal policy": func(t *testing.T, fixture *slotPackageFixture) {
			fixture.pkg.Bundle.Components[fixture.slotIndex].RemovalPolicy = appauthority.RemovalPolicyRemoveIfUnreferenced
			rehashFixture(t, &fixture.pkg)
		},
		"business key mismatch": func(t *testing.T, fixture *slotPackageFixture) {
			behavior, err := ParseCanonicalBehavior(fixture.pkg.ComponentBlobs[fixture.slotIndex].CanonicalBehavior)
			if err != nil {
				t.Fatal(err)
			}
			behavior.LocalComponentKey = layoutbundlekeyv1.LocalComponentKey("key256:" + strings.Repeat("2", 64))
			raw, err := CanonicalBehaviorBytes(behavior)
			if err != nil {
				t.Fatal(err)
			}
			setSlotBehavior(t, fixture, raw)
		},
		"noncanonical behavior": func(t *testing.T, fixture *slotPackageFixture) {
			raw := append([]byte(" "), fixture.pkg.ComponentBlobs[fixture.slotIndex].CanonicalBehavior...)
			setSlotBehavior(t, fixture, raw)
		},
	} {
		t.Run(name, func(t *testing.T) {
			fixture := newSlotPackageFixture(t, 1, "Issue", nil)
			mutate(t, &fixture)
			if _, _, err := VerifyPackageDeclarations(fixture.pkg); err == nil {
				t.Fatal("VerifyPackageDeclarations succeeded")
			}
		})
	}
}

func TestVerifyPackageDeclarationsRejectsVersionPredecessorMismatch(t *testing.T) {
	initial := newSlotPackageFixture(t, 1, "Issue", pointer("sha256:"+strings.Repeat("a", 64)))
	if _, _, err := VerifyPackageDeclarations(initial.pkg); err == nil || !strings.Contains(err.Error(), "must omit") {
		t.Fatalf("v1 predecessor error = %v", err)
	}
	successor := newSlotPackageFixture(t, 2, "IssueV2", nil)
	if _, _, err := VerifyPackageDeclarations(successor.pkg); err == nil || !strings.Contains(err.Error(), "requires") {
		t.Fatalf("v2 predecessor error = %v", err)
	}
}

func TestVerifyPackageDeclarationsRejectsDuplicateLocalKeyAcrossNamespaces(t *testing.T) {
	fixture := newSlotPackageFixture(t, 1, "Issue", nil)
	originalSlot := fixture.pkg.Bundle.Components[fixture.slotIndex]
	duplicateSlot := originalSlot
	duplicateSlot.NamespaceID = "io.example.alt"
	duplicateSlot.ComponentID = appauthority.ComponentID(duplicateSlot.NamespaceID, duplicateSlot.ComponentKind, duplicateSlot.BusinessKey)
	duplicateBehavior := append([]byte(nil), fixture.pkg.ComponentBlobs[fixture.slotIndex].CanonicalBehavior...)
	pairs := make([]fixtureComponent, 0, 3)
	for index, component := range fixture.pkg.Bundle.Components {
		pairs = append(pairs, fixtureComponent{component: component, behavior: fixture.pkg.ComponentBlobs[index].CanonicalBehavior})
	}
	pairs = append(pairs, fixtureComponent{component: duplicateSlot, behavior: duplicateBehavior})
	sort.Slice(pairs, func(left, right int) bool {
		return pairs[left].component.ComponentID < pairs[right].component.ComponentID
	})
	fixture.pkg.Bundle.Components = make([]appauthority.Component, len(pairs))
	fixture.pkg.ComponentBlobs = make([]appauthority.ComponentBlob, len(pairs))
	for index, pair := range pairs {
		fixture.pkg.Bundle.Components[index] = pair.component
		fixture.pkg.ComponentBlobs[index] = appauthority.ComponentBlob{ComponentID: pair.component.ComponentID, CanonicalBehavior: pair.behavior}
	}
	fixture.pkg.Manifest.OwnedNamespaces = []string{"io.example.alt", fixtureNamespace}
	rehashFixture(t, &fixture.pkg)
	if _, _, err := VerifyPackageDeclarations(fixture.pkg); err == nil || !strings.Contains(err.Error(), "already declared") {
		t.Fatalf("duplicate LocalComponentKey error = %v", err)
	}
}

func TestVerifyPackageDeclarationsRejectsSlotTarget(t *testing.T) {
	fixture := newSlotPackageFixture(t, 1, "Issue", nil)
	slot := fixture.pkg.Bundle.Components[fixture.slotIndex]
	targetIndex := 1 - fixture.slotIndex
	target := fixture.pkg.Bundle.Components[targetIndex]
	target.ComponentKind = ComponentKind
	target.BusinessKey = "key256:" + strings.Repeat("2", 64)
	target.ComponentID = appauthority.ComponentID(target.NamespaceID, target.ComponentKind, target.BusinessKey)
	target.LocalDependencies = []string{}
	target.OverridePolicy = appauthority.OverridePolicyReplace
	target.RemovalPolicy = appauthority.RemovalPolicyBlockIfReferencedOrOverridden
	targetBehavior, err := CanonicalBehaviorBytes(Behavior{
		SchemaVersion: BehaviorSchemaVersion, LocalComponentKey: layoutbundlekeyv1.LocalComponentKey("key256:" + strings.Repeat("2", 64)), TargetComponentID: slot.ComponentID,
	})
	if err != nil {
		t.Fatal(err)
	}
	target.ContentDigest, err = appauthority.ComponentDigest(ComponentKind, ComponentFormatVersion, targetBehavior)
	if err != nil {
		t.Fatal(err)
	}
	fixture.pkg.Bundle.Components[targetIndex] = target
	fixture.pkg.ComponentBlobs[targetIndex] = appauthority.ComponentBlob{ComponentID: target.ComponentID, CanonicalBehavior: targetBehavior}
	fixture.pkg.Bundle.Components[fixture.slotIndex].LocalDependencies = []string{target.ComponentID}
	behavior, err := ParseCanonicalBehavior(fixture.pkg.ComponentBlobs[fixture.slotIndex].CanonicalBehavior)
	if err != nil {
		t.Fatal(err)
	}
	behavior.TargetComponentID = target.ComponentID
	raw, err := CanonicalBehaviorBytes(behavior)
	if err != nil {
		t.Fatal(err)
	}
	slotDigest, err := appauthority.ComponentDigest(ComponentKind, ComponentFormatVersion, raw)
	if err != nil {
		t.Fatal(err)
	}
	fixture.pkg.Bundle.Components[fixture.slotIndex].ContentDigest = slotDigest
	fixture.pkg.ComponentBlobs[fixture.slotIndex].CanonicalBehavior = raw
	pairs := []fixtureComponent{
		{component: fixture.pkg.Bundle.Components[0], behavior: fixture.pkg.ComponentBlobs[0].CanonicalBehavior},
		{component: fixture.pkg.Bundle.Components[1], behavior: fixture.pkg.ComponentBlobs[1].CanonicalBehavior},
	}
	sort.Slice(pairs, func(left, right int) bool {
		return pairs[left].component.ComponentID < pairs[right].component.ComponentID
	})
	for index, pair := range pairs {
		fixture.pkg.Bundle.Components[index] = pair.component
		fixture.pkg.ComponentBlobs[index] = appauthority.ComponentBlob{ComponentID: pair.component.ComponentID, CanonicalBehavior: pair.behavior}
	}
	rehashFixture(t, &fixture.pkg)
	if _, _, err := VerifyPackageDeclarations(fixture.pkg); err == nil || (!strings.Contains(err.Error(), "non-slot") && !strings.Contains(err.Error(), "singleton")) {
		t.Fatalf("slot target error = %v", err)
	}
}

func TestRevisionDigestCommitsAppAndCompleteDescriptor(t *testing.T) {
	fixture, declaration := fixtureDeclaration(t, 1, "Issue", nil)
	component := fixture.pkg.Bundle.Components[fixture.slotIndex]
	otherAppDigest, err := LayoutBundleSlotRevisionDigest("io.example.other", component)
	if err != nil {
		t.Fatal(err)
	}
	if otherAppDigest == declaration.RevisionDigest {
		t.Fatal("AppID change did not change revision digest")
	}
	component.ContentDigest = "sha256:" + strings.Repeat("f", 64)
	descriptorDigest, err := LayoutBundleSlotRevisionDigest(fixtureAppID, component)
	if err != nil {
		t.Fatal(err)
	}
	if descriptorDigest == declaration.RevisionDigest {
		t.Fatal("descriptor change did not change revision digest")
	}
	canonical, err := appauthority.CanonicalComponentDescriptor(fixture.pkg.Bundle.Components[fixture.slotIndex])
	if err != nil || !bytes.Contains(canonical, []byte(declaration.BehaviorDigest)) {
		t.Fatalf("canonical descriptor = %s, %v", canonical, err)
	}
}

func TestTargetContentChangeRetainingTargetIdentityReusesSlotRevision(t *testing.T) {
	fixture, before := fixtureDeclaration(t, 1, "Issue", nil)
	targetIndex := 1 - fixture.slotIndex
	target := fixture.pkg.Bundle.Components[targetIndex]
	changedBehavior := []byte(`{"fields":["Name","Owner"]}`)
	changedDigest, err := appauthority.ComponentDigest(target.ComponentKind, 2, changedBehavior)
	if err != nil {
		t.Fatal(err)
	}
	target.ComponentFormatVersion = 2
	target.ContentDigest = changedDigest
	fixture.pkg.Bundle.Components[targetIndex] = target
	fixture.pkg.ComponentBlobs[targetIndex].CanonicalBehavior = changedBehavior
	rehashFixture(t, &fixture.pkg)
	_, declarations, err := VerifyPackageDeclarations(fixture.pkg)
	if err != nil {
		t.Fatal(err)
	}
	if len(declarations) != 1 || declarations[0].RevisionDigest != before.RevisionDigest || declarations[0].TargetComponentID != before.TargetComponentID {
		t.Fatalf("target content change advanced slot revision: before=%#v after=%#v", before, declarations)
	}
}
