package catalogtenant

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

	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"
	tenantauthorityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/tenantlayoutbundleauthorityv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogpublication"
	"github.com/pingcap-inc/nexuscrm-controlplane/internal/catalogverificationtx"
)

func TestDeriveTenantLayoutBundleOwnerValuesBindsSourceAndLockedAuthority(
	t *testing.T,
) {
	source, owner := tenantOwnerVerificationFixture(t)
	if err := validateTenantOwnerFactAgainstSource(source, owner); err != nil {
		t.Fatal(err)
	}

	evidence, evidenceDigest, receipt, receiptDigest, err :=
		deriveTenantLayoutBundleOwnerValues(source, owner)
	if err != nil {
		t.Fatal(err)
	}
	if err := tenantauthorityv1.ValidateEvidence(evidence); err != nil {
		t.Fatal(err)
	}
	if evidence.SourceCatalogSnapshot != source.SourceCatalogSnapshot ||
		evidence.BundleKey != owner.Slot.BundleKey ||
		evidence.NamespaceReservationValueVersion != owner.NamespaceReservation.ValueVersion ||
		evidence.NamespaceReservationState != owner.NamespaceReservation.State ||
		evidence.OrgBindingValueVersion != owner.OrgBinding.ValueVersion ||
		evidence.OrgBindingState != owner.OrgBinding.State ||
		evidence.SlotValueVersion != owner.Slot.ValueVersion ||
		evidence.SlotState != owner.Slot.State {
		t.Fatalf("evidence = %#v", evidence)
	}
	recomputedEvidence, err := tenantauthorityv1.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 != source.BundleKey ||
		receipt.EvidenceVersion != evidence.EvidenceVersion ||
		receipt.EvidenceDigest != evidenceDigest ||
		receipt.VerificationState != layoutbundleownerverificationv1.VerificationStateVerified {
		t.Fatalf("receipt = %#v", receipt)
	}
	recomputedReceipt, err := layoutbundleownerverificationv1.ReceiptDigest(receipt)
	if err != nil || recomputedReceipt != receiptDigest {
		t.Fatalf("receipt digest = %q, %v", recomputedReceipt, err)
	}
}

func TestTenantOwnerFactValidationBindsPrivateDigestsAndSourceIdentity(t *testing.T) {
	source, owner := tenantOwnerVerificationFixture(t)
	if err := validateTenantOwnerFactAgainstSource(source, owner); err != nil {
		t.Fatal(err)
	}

	digestDrift := owner
	digestDrift.SlotDigest = tenantOwnerVerificationDigest("9")
	if err := validateTenantOwnerFactAgainstSource(source, digestDrift); !errors.Is(
		err,
		ErrCorrupt,
	) {
		t.Fatalf("slot digest drift = %v", err)
	}

	foreignBinding := owner
	foreignBinding.OrgBinding.OrgID = "org-2"
	foreignBinding.OrgBindingDigest = mustBindingDigest(
		t,
		foreignBinding.OrgBinding,
	)
	if err := validateTenantOwnerFactAgainstSource(source, foreignBinding); !errors.Is(
		err,
		ErrConflict,
	) {
		t.Fatalf("foreign binding = %v", err)
	}

	foreignSnapshot := source
	foreignSnapshot.SourceCatalogSnapshot.OrgID = "org-2"
	if err := validateTenantOwnerFactAgainstSource(foreignSnapshot, owner); !errors.Is(
		err,
		ErrConflict,
	) {
		t.Fatalf("foreign source snapshot = %v", err)
	}

	zeroFence := owner
	zeroFence.SlotCreationCommandSequence = 0
	if err := validateTenantOwnerFactAgainstSource(source, zeroFence); !errors.Is(
		err,
		ErrCorrupt,
	) {
		t.Fatalf("zero authority fact version = %v", err)
	}
}

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

func TestTenantOwnerVerifierPublicSurfaceIsFixedPurpose(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 tenant verifier contains forbidden seam %q", forbidden)
		}
	}
}

func TestMapTenantOwnerVerificationError(t *testing.T) {
	for name, test := range map[string]struct {
		input error
		want  error
	}{
		"not found": {
			input: catalogverificationtx.ErrTenantOwnerNotFound,
			want:  ErrNotFound,
		},
		"owner conflict": {
			input: catalogverificationtx.ErrTenantOwnerConflict,
			want:  ErrConflict,
		},
		"closed source": {
			input: catalogverificationtx.ErrClosed,
			want:  ErrConflict,
		},
		"owner corrupt": {
			input: catalogverificationtx.ErrTenantOwnerCorrupt,
			want:  ErrCorrupt,
		},
		"source corrupt": {
			input: catalogpublication.ErrSourceCorrupt,
			want:  ErrCorrupt,
		},
	} {
		t.Run(name, func(t *testing.T) {
			if err := mapTenantOwnerVerificationError(test.input); !errors.Is(
				err,
				test.want,
			) {
				t.Fatalf("mapped error = %v, want %v", err, test.want)
			}
		})
	}
}

func tenantOwnerVerificationFixture(
	t *testing.T,
) (
	catalogverificationtx.TenantSourceFacts,
	catalogverificationtx.TenantOwnerFact,
) {
	t.Helper()
	key := layoutbundlekeyv1.BundleKey{
		SlotOwnerKind:      layoutbundlekeyv1.SlotOwnerKindTenant,
		SlotOwnerNamespace: testNamespace,
		LocalComponentKey:  testLocalKey,
	}
	source := catalogverificationtx.TenantSourceFacts{
		SourceCatalogSnapshot: catalogpublicationv1.CatalogSnapshotBinding{
			OrgID:                 testOrg,
			CatalogRevision:       7,
			CatalogSnapshotDigest: tenantOwnerVerificationDigest("1"),
			SchemaGeneration:      4,
			EngineGeneration:      5,
		},
		OrgID:                       testOrg,
		WriterCommandID:             "018f0000-0000-7000-9000-000000000001",
		WriterReservationGeneration: 3,
		SourceRequestDigest:         tenantOwnerVerificationDigest("2"),
		RequiredBundleKeySetDigest:  tenantOwnerVerificationDigest("3"),
		BundleKey:                   key,
	}
	namespace := namespaceValue(
		testNamespace,
		tenantauthorityv1.NamespaceStateActive,
	)
	binding := bindingValue(
		testOrg,
		testNamespace,
		tenantauthorityv1.OrgBindingStateActive,
	)
	slot := slotValue(testNamespace, tenantauthorityv1.SlotStateActive)
	owner := catalogverificationtx.TenantOwnerFact{
		SlotCreationCommandSequence: 3,
		NamespaceReservation:        namespace,
		NamespaceReservationDigest:  mustNamespaceDigest(t, namespace),
		OrgBinding:                  binding,
		OrgBindingDigest:            mustBindingDigest(t, binding),
		Slot:                        slot,
		SlotDigest:                  mustSlotDigest(t, slot),
	}
	return source, owner
}

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