package releaseartifact

import (
	"bytes"
	"crypto/ed25519"
	"crypto/sha256"
	"encoding/hex"
	"strings"
	"testing"
	"time"

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

func TestPackageArtifactDeterministicIdentityAndAttestation(t *testing.T) {
	pkg := testPackage(t)
	first, firstRaw, err := BuildPackage(pkg)
	if err != nil {
		t.Fatal(err)
	}
	second, secondRaw, err := BuildPackage(pkg)
	if err != nil {
		t.Fatal(err)
	}
	if first.AppReleaseDigest != second.AppReleaseDigest || !bytes.Equal(firstRaw, secondRaw) {
		t.Fatal("identical inputs did not produce a byte-identical AppPackage artifact and digest")
	}
	_, verifiedPackage, err := ParsePackage(firstRaw)
	if err != nil {
		t.Fatal(err)
	}
	publicKey, privateKey := deterministicKey(t)
	provenance := testProvenance(firstRaw)
	attestation, attestationRaw, err := SignAttestation(verifiedPackage, provenance, "release-key-1", time.Unix(1_800_000_000, 0).UTC(), privateKey)
	if err != nil {
		t.Fatal(err)
	}
	_, secondAttestation, err := SignAttestation(verifiedPackage, provenance, "release-key-1", time.Unix(1_800_000_000, 0).UTC(), privateKey)
	if err != nil {
		t.Fatal(err)
	}
	if !bytes.Equal(attestationRaw, secondAttestation) {
		t.Fatal("deterministic Ed25519 attestation changed for identical inputs")
	}
	verified, err := Verify(firstRaw, attestationRaw, publicKey)
	if err != nil {
		t.Fatal(err)
	}
	if verified.Package.AppReleaseDigest != first.AppReleaseDigest || verified.Attestation.KeyID != attestation.KeyID ||
		!verified.Attestation.IssuedAt.Equal(time.Unix(1_800_000_000, 0).UTC()) {
		t.Fatalf("verified artifact changed identity: %#v", verified)
	}
}

func TestPackageArtifactRejectsTamperingDigestAndAmbiguousShape(t *testing.T) {
	_, packageRaw, err := BuildPackage(testPackage(t))
	if err != nil {
		t.Fatal(err)
	}
	_, pkg, err := ParsePackage(packageRaw)
	if err != nil {
		t.Fatal(err)
	}
	publicKey, privateKey := deterministicKey(t)
	_, attestationRaw, err := SignAttestation(pkg, testProvenance(packageRaw), "release-key-1", time.Unix(1_800_000_000, 0).UTC(), privateKey)
	if err != nil {
		t.Fatal(err)
	}

	tamperedPackage := bytes.Replace(packageRaw, []byte(`ImFjdGl2ZSI`), []byte(`ImFjdGl2ZSI`), 1)
	if bytes.Equal(tamperedPackage, packageRaw) {
		// Change one base64 byte without changing JSON shape.
		marker := []byte(`"canonical_behavior":"`)
		position := bytes.Index(packageRaw, marker)
		if position < 0 {
			t.Fatal("component blob marker missing")
		}
		tamperedPackage = bytes.Clone(packageRaw)
		position += len(marker)
		if tamperedPackage[position] == 'A' {
			tamperedPackage[position] = 'B'
		} else {
			tamperedPackage[position] = 'A'
		}
	}
	if _, err := Verify(tamperedPackage, attestationRaw, publicKey); err == nil {
		t.Fatal("tampered component content was accepted")
	}

	badDigest := bytes.Replace(packageRaw, []byte(pkg.AppReleaseDigest), []byte("sha256:"+strings.Repeat("0", 64)), 1)
	if _, _, err := ParsePackage(badDigest); err == nil || !strings.Contains(err.Error(), "digest mismatch") {
		t.Fatalf("declared root digest mismatch error = %v", err)
	}

	tamperedAttestation := bytes.Replace(attestationRaw, []byte(`"source_path":"metadata"`), []byte(`"source_path":"other"`), 1)
	if _, err := Verify(packageRaw, tamperedAttestation, publicKey); err == nil || !strings.Contains(err.Error(), "signature") {
		t.Fatalf("tampered provenance error = %v", err)
	}

	unknown := bytes.Replace(packageRaw, []byte(`{"schema_version":`), []byte(`{"unknown":true,"schema_version":`), 1)
	if _, _, err := ParsePackage(unknown); err == nil || !strings.Contains(err.Error(), "canonical") {
		t.Fatalf("unknown ownership field error = %v", err)
	}
	duplicate := bytes.Replace(packageRaw, []byte(`{"schema_version":`), []byte(`{"schema_version":"wrong","schema_version":`), 1)
	if _, _, err := ParsePackage(duplicate); err == nil {
		t.Fatal("duplicate ownership field was accepted")
	}
}

func TestCompatibilityRejectsMissingAndIncompatibleRequirements(t *testing.T) {
	host := HostCompatibility{
		Protocols:          []SupportedProtocol{{ProtocolID: "nexuscrm.runtime.v1", Major: 1, Minor: 1}},
		EngineCapabilities: []string{"runtime.records.evaluate-access.v1"},
		ComponentKinds:     []SupportedComponentKind{{ComponentKind: "metadata.object.v1", ComponentFormatVersion: 1}},
	}
	pkg, err := appauthority.VerifyPackage(testPackage(t))
	if err != nil {
		t.Fatal(err)
	}
	if err := VerifyCompatibility(pkg, host); err != nil {
		t.Fatal(err)
	}
	host.Protocols[0].Major = 2
	if err := VerifyCompatibility(pkg, host); err == nil || !strings.Contains(err.Error(), "incompatible") {
		t.Fatalf("protocol-major mismatch error = %v", err)
	}
	host.Protocols[0].Major = 1
	host.EngineCapabilities = []string{}
	if err := VerifyCompatibility(pkg, host); err == nil || !strings.Contains(err.Error(), "unavailable") {
		t.Fatalf("missing engine capability error = %v", err)
	}
	host.EngineCapabilities = []string{"runtime.records.evaluate-access.v1", "runtime.records.evaluate-access.v1"}
	if err := VerifyCompatibility(pkg, host); err == nil || !strings.Contains(err.Error(), "duplicate") {
		t.Fatalf("duplicate host capability error = %v", err)
	}
	host.EngineCapabilities = []string{"runtime.records.evaluate-access.v1"}
	host.ComponentKinds = []SupportedComponentKind{}
	if err := VerifyCompatibility(pkg, host); err == nil || !strings.Contains(err.Error(), "component kind") {
		t.Fatalf("missing component format error = %v", err)
	}
}

func testPackage(t *testing.T) appauthority.Package {
	t.Helper()
	behavior := []byte(`{"fields":["active"]}`)
	contentDigest, err := appauthority.ComponentDigest("metadata.object.v1", 1, behavior)
	if err != nil {
		t.Fatal(err)
	}
	component := appauthority.Component{
		ComponentKind: "metadata.object.v1", ComponentFormatVersion: 1, NamespaceID: "io.example.crm",
		BusinessKey: "WorkItem", DefinitionVersion: "1", LocalDependencies: []string{},
		ExternalComponentRefs: []appauthority.ExternalComponentRef{}, OverridePolicy: appauthority.OverridePolicyReplace,
		RemovalPolicy: appauthority.RemovalPolicyBlockIfReferencedOrOverridden, ContentDigest: contentDigest,
	}
	component.ComponentID = appauthority.ComponentID(component.NamespaceID, component.ComponentKind, component.BusinessKey)
	bundle := appauthority.MetadataBundle{SchemaVersion: appauthority.MetadataBundleSchemaVersion, Components: []appauthority.Component{component}}
	manifest := appauthority.AppManifest{
		SchemaVersion: appauthority.AppManifestSchemaVersion, AppID: "io.example.crm", PublisherID: "io.example",
		SemanticVersion: "1.0.0", MetadataBundleDigest: "sha256:" + strings.Repeat("0", 64), Dependencies: []appauthority.Dependency{},
		OwnedNamespaces:            []string{"io.example.crm"},
		RequiredProtocols:          []appauthority.ProtocolRequirement{{ProtocolID: "nexuscrm.runtime.v1", Major: 1, MinimumMinor: 1}},
		RequiredEngineCapabilities: []string{"runtime.records.evaluate-access.v1"},
		ProvidedCapabilities:       []appauthority.ProvidedCapability{{CapabilityID: "io.example.crm.ui.access.v1", Kind: appauthority.CapabilityKindServiceHandler, HandlerRef: "io.example.crm.handler.ui.access.v1", RequiredRuntimeCapabilities: []string{"runtime.records.evaluate-access.v1"}}},
		FrontendCompatibility:      appauthority.FrontendCompatibility{Mode: appauthority.FrontendModeNone},
	}
	manifest.MetadataBundleDigest, err = appauthority.MetadataBundleDigest(bundle, manifest)
	if err != nil {
		t.Fatal(err)
	}
	return appauthority.Package{Manifest: manifest, Bundle: bundle, ComponentBlobs: []appauthority.ComponentBlob{{ComponentID: component.ComponentID, CanonicalBehavior: behavior}}}
}

func deterministicKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
	t.Helper()
	seed := sha256.Sum256([]byte("nexuscrm-releaseartifact-test-key"))
	privateKey := ed25519.NewKeyFromSeed(seed[:])
	return privateKey.Public().(ed25519.PublicKey), privateKey
}

func testProvenance(packageRaw []byte) Provenance {
	digest := sha256.Sum256(packageRaw)
	return Provenance{SchemaVersion: ProvenanceSchemaVersion, SourceRepository: "example/app", SourceRevision: "revision-1", SourcePath: "metadata", SourceTreeDigest: "sha256:" + hex.EncodeToString(digest[:]), BuilderID: "example/builder/v1", ContractModuleVersion: "v0.1.8"}
}
