package catalogapp

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"testing"

	applayoutbundleauthorityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/applayoutbundleauthorityv1"
	catalogpublicationv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalogpublicationv1"
	layoutbundlekeyv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundlekeyv1"
	layoutbundleownerverificationv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundleownerverificationv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogpublication"
	"github.com/pingcap-inc/nexuscrm-controlplane/internal/catalogverificationtx"
)

func TestDeriveAppLayoutBundleOwnerValuesBindsCompleteSourceAndOwnerFact(
	t *testing.T,
) {
	key := layoutbundlekeyv1.BundleKey{
		SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindApp,
		SlotOwnerNamespace: "io.example.alpha",
		LocalComponentKey: layoutbundlekeyv1.LocalComponentKey(
			"key256:" + strings.Repeat("1", 64),
		),
	}
	source := catalogverificationtx.AppSourceFacts{
		SourceCatalogSnapshot: catalogpublicationv1.CatalogSnapshotBinding{
			OrgID:                 "org-1",
			CatalogRevision:       7,
			CatalogSnapshotDigest: ownerVerificationDigest("1"),
			SchemaGeneration:      4,
			EngineGeneration:      5,
		},
		OrgID:                       "org-1",
		WriterCommandID:             "018f0000-0000-7000-9000-000000000001",
		WriterReservationGeneration: 3,
		SourceRequestDigest:         ownerVerificationDigest("2"),
		RequiredBundleKeySetDigest:  ownerVerificationDigest("3"),
		BundleKey:                   key,
		AppID:                       key.SlotOwnerNamespace,
		AppInstallationID:           "018f0000-0000-7000-9000-000000000011",
		AppReleaseDigest:            ownerVerificationDigest("4"),
	}
	owner := catalogverificationtx.AppOwnerFact{
		AppInstallationID:              source.AppInstallationID,
		AppReleaseDigest:               source.AppReleaseDigest,
		DeclarationComponentID:         ownerVerificationDigest("5"),
		DefinitionVersion:              9,
		LayoutBundleSlotRevisionDigest: ownerVerificationDigest("6"),
		CatalogRevision:                7,
		ActivationEpoch:                2,
	}

	evidence, evidenceDigest, receipt, receiptDigest, err :=
		deriveAppLayoutBundleOwnerValues(source, owner)
	if err != nil {
		t.Fatal(err)
	}
	if err := applayoutbundleauthorityv1.ValidateEvidence(evidence); err != nil {
		t.Fatal(err)
	}
	if evidence.SourceCatalogSnapshot != source.SourceCatalogSnapshot ||
		evidence.AppInstallationID != owner.AppInstallationID ||
		evidence.AppReleaseDigest != owner.AppReleaseDigest ||
		evidence.DeclarationComponentID != owner.DeclarationComponentID ||
		evidence.DefinitionVersion != owner.DefinitionVersion ||
		evidence.LayoutBundleSlotRevisionDigest != owner.LayoutBundleSlotRevisionDigest {
		t.Fatalf("evidence = %#v", evidence)
	}
	recomputedEvidence, err := applayoutbundleauthorityv1.EvidenceDigest(evidence)
	if err != nil || recomputedEvidence != evidenceDigest {
		t.Fatalf("evidence digest = %q, %v", recomputedEvidence, err)
	}
	if err := layoutbundleownerverificationv1.ValidateReceipt(receipt); err != nil {
		t.Fatal(err)
	}
	if receipt.OrgID != source.OrgID ||
		receipt.WriterCommandID != source.WriterCommandID ||
		receipt.WriterReservationGeneration != source.WriterReservationGeneration ||
		receipt.SourceRequestDigest != source.SourceRequestDigest ||
		receipt.RequiredBundleKeySetDigest != source.RequiredBundleKeySetDigest ||
		receipt.BundleKey != key ||
		receipt.EvidenceVersion != evidence.EvidenceVersion ||
		receipt.EvidenceDigest != evidenceDigest {
		t.Fatalf("receipt = %#v", receipt)
	}
	recomputedReceipt, err := layoutbundleownerverificationv1.ReceiptDigest(receipt)
	if err != nil || recomputedReceipt != receiptDigest {
		t.Fatalf("receipt digest = %q, %v", recomputedReceipt, err)
	}
}

func TestVerifiedAppEvidenceCapabilityIsOpaqueAndHasNoWireForm(t *testing.T) {
	capabilityType := reflect.TypeOf(VerifiedAppLayoutBundleEvidenceCapability{})
	for index := 0; index < capabilityType.NumField(); index++ {
		if capabilityType.Field(index).PkgPath == "" {
			t.Fatalf("capability field %q is exported", capabilityType.Field(index).Name)
		}
	}
	capability := &VerifiedAppLayoutBundleEvidenceCapability{}
	if err := capability.Validate(); !errors.Is(err, ErrConflict) {
		t.Fatalf("zero capability validation = %v", err)
	}
	if _, err := json.Marshal(capability); !errors.Is(
		err,
		errVerifiedAppEvidenceHasNoJSON,
	) {
		t.Fatalf("capability JSON marshal = %v", err)
	}
	if err := json.Unmarshal([]byte(`{}`), capability); !errors.Is(
		err,
		errVerifiedAppEvidenceHasNoJSON,
	) {
		t.Fatalf("capability JSON unmarshal = %v", err)
	}
}

func TestAppOwnerVerifierPublicSurfaceIsFixedPurpose(t *testing.T) {
	method, found := reflect.TypeOf((*Authority)(nil)).MethodByName(
		"VerifyLayoutBundleOwner",
	)
	if !found {
		t.Fatal("VerifyLayoutBundleOwner is absent")
	}
	expected := []reflect.Type{
		reflect.TypeOf((*Authority)(nil)),
		reflect.TypeOf((*context.Context)(nil)).Elem(),
		reflect.TypeOf((*catalogpublication.CandidateSourceVerificationLease)(nil)),
		reflect.TypeOf(layoutbundlekeyv1.BundleKey{}),
	}
	if method.Type.NumIn() != len(expected) {
		t.Fatalf("VerifyLayoutBundleOwner inputs = %s", method.Type)
	}
	for index, wanted := range expected {
		if method.Type.In(index) != wanted {
			t.Fatalf(
				"VerifyLayoutBundleOwner input[%d] = %s, want %s",
				index,
				method.Type.In(index),
				wanted,
			)
		}
	}

	_, file, _, ok := runtime.Caller(0)
	if !ok {
		t.Fatal("resolve verifier test file")
	}
	raw, err := os.ReadFile(filepath.Join(
		filepath.Dir(file),
		"layout_bundle_owner_verification.go",
	))
	if err != nil {
		t.Fatal(err)
	}
	for _, forbidden := range []string{
		"database/sql",
		"_System_",
		"QueryContext",
		"ExecContext",
		"ProofAdapter",
		"VerificationCallback",
	} {
		if strings.Contains(string(raw), forbidden) {
			t.Fatalf("public App verifier contains forbidden seam %q", forbidden)
		}
	}
}

func ownerVerificationDigest(digit string) string {
	return "sha256:" + strings.Repeat(digit, 64)
}
