package appauthority

import (
	"bytes"
	"encoding/json"
	"strings"
	"testing"
)

func TestCanonicalPackageRoundTripAndDigestDomains(t *testing.T) {
	manifest, bundle := testPackage(t)
	verified, err := VerifyPackage(testPackageGraph(manifest, bundle))
	if err != nil {
		t.Fatal(err)
	}
	parsedManifest, err := ParseAppManifest(verified.CanonicalManifest)
	if err != nil {
		t.Fatal(err)
	}
	parsedBundle, err := ParseMetadataBundle(verified.CanonicalBundle, parsedManifest)
	if err != nil {
		t.Fatal(err)
	}
	if parsedManifest.AppID != manifest.AppID || len(parsedBundle.Components) != 1 {
		t.Fatalf("round trip changed package: %#v %#v", parsedManifest, parsedBundle)
	}
	if verified.AppReleaseDigest == manifest.MetadataBundleDigest || verified.AppReleaseDigest == bundle.Components[0].ContentDigest {
		t.Fatal("digest domains collided")
	}
}

func TestImmutableConformanceVector(t *testing.T) {
	manifest, bundle := testPackage(t)
	verified, err := VerifyPackage(testPackageGraph(manifest, bundle))
	if err != nil {
		t.Fatal(err)
	}
	var vectors struct {
		SchemaVersion string `json:"schema_version"`
		Vectors       []struct {
			CanonicalManifest string `json:"canonical_manifest"`
			CanonicalBundle   string `json:"canonical_bundle"`
			AppReleaseDigest  string `json:"app_release_digest"`
		} `json:"vectors"`
	}
	if err := json.Unmarshal(ConformanceVectorsJSON(), &vectors); err != nil {
		t.Fatal(err)
	}
	if vectors.SchemaVersion != "nexuscrm.app-authority.conformance/v1" || len(vectors.Vectors) != 1 {
		t.Fatalf("unexpected vectors envelope: %#v", vectors)
	}
	vector := vectors.Vectors[0]
	if vector.CanonicalManifest != string(verified.CanonicalManifest) || vector.CanonicalBundle != string(verified.CanonicalBundle) || vector.AppReleaseDigest != verified.AppReleaseDigest {
		t.Fatalf("immutable vector drifted:\nvector=%#v\ncanonical_manifest=%s\ncanonical_bundle=%s\napp_release_digest=%s", vector, verified.CanonicalManifest, verified.CanonicalBundle, verified.AppReleaseDigest)
	}
}

func TestStrictParsingRejectsUnknownAndDuplicateMembers(t *testing.T) {
	manifest, bundle := testPackage(t)
	canonical, err := CanonicalAppManifest(manifest)
	if err != nil {
		t.Fatal(err)
	}
	unknown := bytes.Replace(canonical, []byte(`{"app_id":`), []byte(`{"unknown":true,"app_id":`), 1)
	if _, err := ParseAppManifest(unknown); err == nil || !strings.Contains(err.Error(), "unknown field") {
		t.Fatalf("unknown member error = %v", err)
	}
	duplicate := bytes.Replace(canonical, []byte(`{"app_id":`), []byte(`{"app_id":"io.fake.app","app_id":`), 1)
	if _, err := ParseAppManifest(duplicate); err == nil || !strings.Contains(err.Error(), "duplicate JSON member") {
		t.Fatalf("duplicate member error = %v", err)
	}
	bundleCanonical, err := CanonicalMetadataBundle(bundle, manifest)
	if err != nil {
		t.Fatal(err)
	}
	if bytes.Contains(bundleCanonical, []byte("AppReleaseDigest")) {
		t.Fatal("bundle contains release identity")
	}
}

func TestStrictParsingRejectsMissingAndNullClosedShapeFields(t *testing.T) {
	manifest, bundle := testPackage(t)
	bundleCanonical, err := CanonicalMetadataBundle(bundle, manifest)
	if err != nil {
		t.Fatal(err)
	}
	missing := bytes.Replace(bundleCanonical, []byte(`,"override_policy":"preserve-authorized-overrides"`), nil, 1)
	if _, err := ParseMetadataBundle(missing, manifest); err == nil || !strings.Contains(err.Error(), "exactly fields") {
		t.Fatalf("missing policy error = %v", err)
	}
	nullValue := bytes.Replace(bundleCanonical, []byte(`"override_policy":"preserve-authorized-overrides"`), []byte(`"override_policy":null`), 1)
	if _, err := ParseMetadataBundle(nullValue, manifest); err == nil || !strings.Contains(err.Error(), "cannot be null") {
		t.Fatalf("null policy error = %v", err)
	}
}

func TestStrictParsingRejectsMalformedUnicodeBeforeNormalization(t *testing.T) {
	manifest, _ := testPackage(t)
	canonical, err := CanonicalAppManifest(manifest)
	if err != nil {
		t.Fatal(err)
	}
	for name, malformed := range map[string][]byte{
		"invalid UTF-8":           append([]byte(`{"schema_version":"`), 0xff),
		"lone high surrogate":     bytes.Replace(canonical, []byte(`"io.example.app"`), []byte(`"io.example.\ud800"`), 1),
		"lone low surrogate":      bytes.Replace(canonical, []byte(`"io.example.app"`), []byte(`"io.example.\udc00"`), 1),
		"high followed by scalar": bytes.Replace(canonical, []byte(`"io.example.app"`), []byte(`"io.example.\ud800\u0061"`), 1),
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := ParseAppManifest(malformed); err == nil || !strings.Contains(err.Error(), "UTF") {
				t.Fatalf("ParseAppManifest error = %v", err)
			}
		})
	}
	validPair := bytes.Replace(canonical, []byte(`"io.example.app"`), []byte(`"io.example.\ud83d\ude00"`), 1)
	if _, err := ParseAppManifest(validPair); err == nil || strings.Contains(err.Error(), "UTF-16") {
		t.Fatalf("valid surrogate pair was rejected as malformed Unicode: %v", err)
	}
	escapedLiteral := bytes.Replace(canonical, []byte(`"io.example.app"`), []byte(`"io.example.\\ud800"`), 1)
	if _, err := ParseAppManifest(escapedLiteral); err == nil || strings.Contains(err.Error(), "UTF-16") {
		t.Fatalf("escaped backslash was interpreted as a surrogate escape: %v", err)
	}
}

func TestValidationRejectsAmbiguousSetsAndIdentifiers(t *testing.T) {
	manifest, _ := testPackage(t)
	manifest.RequiredEngineCapabilities = []string{"runtime.records.evaluate-access.v1", "runtime.records.evaluate-access.v1"}
	if err := manifest.Validate(); err == nil || !strings.Contains(err.Error(), "strictly bytewise sorted") {
		t.Fatalf("duplicate capability error = %v", err)
	}
	for _, invalid := range []string{" io.example.app", "Io.example.app", "io.example", "io.example.app-", "io.exämple.app"} {
		if err := ValidateAppID(invalid); err == nil {
			t.Fatalf("ValidateAppID(%q) succeeded", invalid)
		}
	}
	for _, valid := range []string{"018f22b2-7c00-7000-8000-000000000001", "ffffffff-ffff-7fff-bfff-ffffffffffff"} {
		if err := ValidateAppInstallationID(valid); err != nil {
			t.Fatalf("ValidateAppInstallationID(%q): %v", valid, err)
		}
	}
}

func TestValidateComponentProjectionBindsExistingComponentIdentityAndOpaqueBehavior(t *testing.T) {
	behavior := []byte{0xff, 0x00, 0x01}
	component := Component{
		ComponentKind: "metadata.pack.connector-inbound-route.v1", ComponentFormatVersion: 1,
		NamespaceID: "io.example", BusinessKey: "route-1", DefinitionVersion: "1",
	}
	component.ComponentID = ComponentID(component.NamespaceID, component.ComponentKind, component.BusinessKey)
	var err error
	component.ContentDigest, err = ComponentDigest(component.ComponentKind, component.ComponentFormatVersion, behavior)
	if err != nil {
		t.Fatal(err)
	}
	if err := ValidateComponentProjection(component, behavior); err != nil {
		t.Fatalf("valid projection: %v", err)
	}
	component.ContentDigest = zeroDigest
	if err := ValidateComponentProjection(component, behavior); err == nil {
		t.Fatal("projection with unrelated behavior digest was accepted")
	}
}

func TestBundleRejectsUnknownDependencyAndCycle(t *testing.T) {
	manifest, bundle := testPackage(t)
	bundle.Components[0].LocalDependencies = []string{bundle.Components[0].ComponentID}
	if _, err := CanonicalMetadataBundle(bundle, manifest); err == nil || !strings.Contains(err.Error(), "local dependency") {
		t.Fatalf("self dependency error = %v", err)
	}
}

func TestVerifyPackageRequiresCompleteMatchingComponentGraph(t *testing.T) {
	manifest, bundle := testPackage(t)
	if _, err := VerifyPackage(Package{Manifest: manifest, Bundle: bundle}); err == nil || !strings.Contains(err.Error(), "cover every") {
		t.Fatalf("missing graph error = %v", err)
	}
	pkg := testPackageGraph(manifest, bundle)
	pkg.ComponentBlobs[0].CanonicalBehavior = []byte(`{"fields":["Other"]}`)
	if _, err := VerifyPackage(pkg); err == nil || !strings.Contains(err.Error(), "does not match") {
		t.Fatalf("mismatched blob error = %v", err)
	}
}

func TestJCSStringAndUTF16KeyRules(t *testing.T) {
	encoded, err := canonicalJSON(map[string]any{"\U0001f600": "<>&\u2028", "\ue000": "line\n"})
	if err != nil {
		t.Fatal(err)
	}
	want := "{\"😀\":\"<>& \",\"\":\"line\\n\"}"
	if string(encoded) != want {
		t.Fatalf("canonical JSON = %q, want %q", encoded, want)
	}
}

func TestSemanticVersionValidationAndOrdering(t *testing.T) {
	for _, invalid := range []string{"v1.0.0", "1.0", "01.0.0", "1.0.0+build", "1.0.0-01"} {
		if err := ValidateSemanticVersion(invalid); err == nil {
			t.Fatalf("ValidateSemanticVersion(%q) succeeded", invalid)
		}
	}
	for _, pair := range [][2]string{{"1.0.0-alpha", "1.0.0"}, {"1.0.0-alpha.1", "1.0.0-alpha.beta"}, {"2.0.0", "10.0.0"}} {
		comparison, err := CompareSemanticVersions(pair[0], pair[1])
		if err != nil {
			t.Fatal(err)
		}
		if comparison >= 0 {
			t.Fatalf("CompareSemanticVersions(%q, %q) = %d, want negative", pair[0], pair[1], comparison)
		}
	}
}

func TestValidateNamespaceIDUsesNeutralTwoSegmentGrammar(t *testing.T) {
	for _, valid := range []string{"io.example", "io.example.tenant"} {
		if err := ValidateNamespaceID(valid); err != nil {
			t.Fatalf("ValidateNamespaceID(%q): %v", valid, err)
		}
	}
	for _, invalid := range []string{"tenant", "io.Example", "io.example-", strings.Repeat("a", 129)} {
		if err := ValidateNamespaceID(invalid); err == nil {
			t.Fatalf("ValidateNamespaceID(%q) succeeded", invalid)
		}
	}
}

func testPackage(t *testing.T) (AppManifest, MetadataBundle) {
	t.Helper()
	behavior := []byte(`{"fields":["Name"]}`)
	contentDigest, err := ComponentDigest("metadata.object.v1", 1, behavior)
	if err != nil {
		t.Fatal(err)
	}
	component := Component{
		ComponentKind: "metadata.object.v1", ComponentFormatVersion: 1,
		NamespaceID: "io.example.app", BusinessKey: "Issue", DefinitionVersion: "1",
		LocalDependencies: []string{}, ExternalComponentRefs: []ExternalComponentRef{},
		OverridePolicy: OverridePolicyPreserveAuthorized,
		RemovalPolicy:  RemovalPolicyBlockIfReferencedOrOverridden, ContentDigest: contentDigest,
	}
	component.ComponentID = ComponentID(component.NamespaceID, component.ComponentKind, component.BusinessKey)
	bundle := MetadataBundle{SchemaVersion: MetadataBundleSchemaVersion, Components: []Component{component}}
	manifest := AppManifest{
		SchemaVersion: AppManifestSchemaVersion, AppID: "io.example.app", PublisherID: "io.example",
		SemanticVersion: "1.0.0", MetadataBundleDigest: zeroDigest,
		Dependencies: []Dependency{}, OwnedNamespaces: []string{"io.example.app"},
		RequiredProtocols:          []ProtocolRequirement{{ProtocolID: "nexuscrm.runtime.v1", Major: 1}},
		RequiredEngineCapabilities: []string{"runtime.records.evaluate-access.v1"},
		ProvidedCapabilities:       []ProvidedCapability{{CapabilityID: "io.example.app.ui.record-access.v1", Kind: CapabilityKindServiceHandler, HandlerRef: "io.example.app.handler.ui.record-access.v1", RequiredRuntimeCapabilities: []string{"runtime.records.evaluate-access.v1"}}},
		FrontendCompatibility:      FrontendCompatibility{Mode: FrontendModeNone},
	}
	digest, err := MetadataBundleDigest(bundle, manifest)
	if err != nil {
		t.Fatal(err)
	}
	manifest.MetadataBundleDigest = digest
	return manifest, bundle
}

func testPackageGraph(manifest AppManifest, bundle MetadataBundle) Package {
	return Package{
		Manifest: manifest,
		Bundle:   bundle,
		ComponentBlobs: []ComponentBlob{{
			ComponentID:       bundle.Components[0].ComponentID,
			CanonicalBehavior: []byte(`{"fields":["Name"]}`),
		}},
	}
}
