package runtimeapi

import (
	"context"
	"testing"

	"github.com/nexuscrm/metadata/catalogsnapshot"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commandsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commandv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	outboundsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/outboundintentv1"
	pagesemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/pagelayoutresolutionv1"
	accesssemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
)

var publicationHandlers = []servingCapabilitySetEntry{
	{testSFCRMAppCapabilityID, accesssemantic.CapabilityName, catalogsnapshot.ProjectionRecordAccessV1},
	{"io.example.permissions.v1", permissionsemantic.CapabilityName, catalogsnapshot.ProjectionEffectivePermissionsV1},
}
var publicationConsumers = []servingCapabilitySetEntry{
	{runtimeCapabilityID: commandsemantic.CapabilityName, profile: catalogsnapshot.ProjectionCommandAcceptanceV1},
	{runtimeCapabilityID: outboundsemantic.CapabilityName, profile: catalogsnapshot.ProjectionRecordMutationV1},
	{runtimeCapabilityID: pagesemantic.CapabilityName, profile: catalogsnapshot.ProjectionPageLayoutResolutionV1},
}

// Publication fixtures keep the original deployment receipts. Only the active
// Catalog tuple, its artifacts, and the security owner's observation advance.
func publicationConsumerState(t *testing.T, initial *servingSetFixture) (*ServingState, *catalogConsumerViewTestEngine) {
	t.Helper()
	receiptArtifact, err := initial.state.store.Get(t.Context(), catalogsnapshot.ArtifactServingCompatibilityReceipt, initial.selections[0].ExpectedCompatibilityReceiptDigest)
	if err != nil {
		t.Fatal(err)
	}
	receipt, err := catalogsnapshot.OpenServingCompatibilityReceipt(receiptArtifact)
	if err != nil {
		t.Fatal(err)
	}
	engine := &catalogConsumerViewTestEngine{}
	selections := make([]ServingConsumerSelection, 0, len(publicationConsumers))
	for _, entry := range publicationConsumers {
		selections = append(selections, ServingConsumerSelection{OrgID: testOrgID, AppInstallationID: testInstallationID, WorkloadID: testWorkloadID, CapabilityID: entry.runtimeCapabilityID, DeploymentSnapshotDigest: receipt.CatalogSnapshotDigest()})
	}
	state, err := NewServingState(ServingStateConfig{Store: initial.state.store, Catalog: initial.state.catalog, Consumers: selections, ConsumerViews: engine})
	if err != nil {
		t.Fatal(err)
	}
	activatePublicationFixture(t, state, engine, initial)
	return state, engine
}

func activatePublicationFixture(t *testing.T, state *ServingState, engine *catalogConsumerViewTestEngine, next *servingSetFixture) {
	t.Helper()
	for _, artifact := range next.state.store.(*servingMemoryStore).artifacts {
		if err := state.store.Put(t.Context(), artifact); err != nil {
			t.Fatal(err)
		}
	}
	state.catalog = next.state.catalog
	artifact, err := next.state.store.Get(t.Context(), catalogsnapshot.ArtifactServingCompatibilityReceipt, next.selections[0].ExpectedCompatibilityReceiptDigest)
	if err != nil {
		t.Fatal(err)
	}
	receipt, err := catalogsnapshot.OpenServingCompatibilityReceipt(artifact)
	if err != nil {
		t.Fatal(err)
	}
	view, err := newServingArtifactSet(state.store).consumerView(t.Context(), receipt.CatalogSnapshotDigest(), testOrgID, testInstallationID, commandsemantic.CapabilityName, next.selections[0].LocalCompatibility)
	if err != nil {
		t.Fatal(err)
	}
	engine.integrationView = commandConsumerIntegrationView(view)
	engine.integrationView.Receipts.SecurityEpoch = next.selections[0].SecurityEpoch
	engine.integrationView.Lineage.ViewDigest = catalogsemantic.ComputeIntegrationBindingViewDigest(engine.integrationView)
}

func TestPublicationRefreshesConsumersAtomicallyAndRecovers(t *testing.T) {
	initial := newServingSetPublicationFixture(t, 7, 1, 0, publicationHandlers, publicationConsumers)
	state, engine := publicationConsumerState(t, initial)
	selections := append([]ServingSelection(nil), initial.selections...)
	if err := state.ReconcileAll(t.Context(), selections); err != nil {
		t.Fatal(err)
	}
	command, outbound, page := state.CommandConsumerAdmission(), state.OutboundIntentAdmission(), state.PageLayoutConsumerAdmission()
	bindAll := func(epoch uint64) []context.Context {
		t.Helper()
		var contexts []context.Context
		for _, bind := range []func() (context.Context, error){
			func() (context.Context, error) {
				return command.bind(commandConsumerTestContext(t, testWorkloadID, epoch))
			},
			func() (context.Context, error) {
				return outbound.bind(outboundIntentTestContext(t, testWorkloadID, epoch))
			},
			func() (context.Context, error) {
				return page.bind(pageLayoutConsumerTrustedContext(t, testWorkloadID, epoch))
			},
		} {
			ctx, err := bind()
			if err != nil {
				t.Fatal(err)
			}
			contexts = append(contexts, ctx)
		}
		return contexts
	}
	inFlight := bindAll(1)
	next := newServingSetPublicationFixture(t, 8, 2, 1, publicationHandlers, publicationConsumers)
	activatePublicationFixture(t, state, engine, next)
	for i := range selections {
		selections[i].SecurityEpoch = 2
	}
	// A valid Integration view with a stale security observation cannot be
	// combined with newly prepared handlers or page metadata.
	engine.integrationView.Receipts.SecurityEpoch = 1
	engine.integrationView.Lineage.ViewDigest = catalogsemantic.ComputeIntegrationBindingViewDigest(engine.integrationView)
	if err := state.ReconcileAll(t.Context(), selections); err == nil || state.Ready() {
		t.Fatal("mixed consumer security epoch became ready")
	}
	if command.prepared() != nil || outbound.prepared() != nil || page.prepared() != nil {
		t.Fatal("failed preparation exposed a consumer")
	}
	engine.integrationView.Receipts.SecurityEpoch = 2
	engine.integrationView.Lineage.ViewDigest = catalogsemantic.ComputeIntegrationBindingViewDigest(engine.integrationView)
	// A missing second handler must also invalidate all three consumers.
	key := servingArtifactKey(catalogsnapshot.ArtifactServingCompatibilityReceipt, next.selections[1].ExpectedCompatibilityReceiptDigest)
	store := state.store.(*servingMemoryStore)
	missing := store.artifacts[key]
	delete(store.artifacts, key)
	if err := state.ReconcileAll(t.Context(), selections); err == nil || state.Ready() {
		t.Fatal("partial publication became ready")
	}
	if command.prepared() != nil || outbound.prepared() != nil || page.prepared() != nil {
		t.Fatal("partial publication exposed a consumer")
	}
	if err := store.Put(t.Context(), missing); err != nil {
		t.Fatal(err)
	}
	if err := state.ReconcileAll(t.Context(), selections); err != nil {
		t.Fatal(err)
	}
	// Once prepared, admissions must not consult Catalog again.
	state.catalog = func(context.Context, string, string) (ServingInstallation, ServingHandlerBinding, bool, error) {
		t.Fatal("request reread Catalog")
		return ServingInstallation{}, ServingHandlerBinding{}, false, nil
	}
	for i, ctx := range bindAll(2) {
		current, _ := runtimeExecutionScopeFromContext(ctx)
		old, _ := runtimeExecutionScopeFromContext(inFlight[i])
		if current.receipts.CatalogRevision != 8 || current.receipts.SecurityEpoch != 2 || old.receipts.CatalogRevision != 7 || old.receipts.SecurityEpoch != 1 || current.view.Digest() == old.view.Digest() {
			t.Fatal("consumer mixed or retained old publication")
		}
	}
	if _, err := page.bind(pageLayoutConsumerTrustedContext(t, testWorkloadID, 1)); err == nil {
		t.Fatal("stale delegated security admitted")
	}
	// Process restart uses exactly the same deployment assertions.
	restarted, err := NewServingState(ServingStateConfig{Store: store, Catalog: next.state.catalog, Consumers: state.consumers, ConsumerViews: engine})
	if err != nil {
		t.Fatal(err)
	}
	if err := restarted.ReconcileAll(t.Context(), selections); err != nil {
		t.Fatalf("original deployment after restart: %v", err)
	}
}

func TestPublicationRejectsChangedDeploymentAndEquivocation(t *testing.T) {
	for _, test := range []struct {
		name                string
		revision, ownership uint64
		mutate              func(*ServingInstallation, *ServingHandlerBinding)
	}{
		{name: "new app release", revision: 8, ownership: 1, mutate: func(i *ServingInstallation, b *ServingHandlerBinding) {
			i.ActiveReleaseDigest = string(catalogsnapshot.ComputeDigest([]byte("new app")))
			b.AppReleaseDigest = i.ActiveReleaseDigest
		}},
		{name: "new service release", revision: 8, ownership: 1, mutate: func(_ *ServingInstallation, b *ServingHandlerBinding) {
			b.ServiceReleaseDigest = string(catalogsnapshot.ComputeDigest([]byte("new service")))
		}},
		{name: "new workload", revision: 8, ownership: 1, mutate: func(_ *ServingInstallation, b *ServingHandlerBinding) { b.WorkloadID = "different-workload" }},
		{name: "reused ownership epoch", revision: 8, ownership: 0},
		{name: "reused catalog revision", revision: 7, ownership: 1},
		{name: "older catalog", revision: 6, ownership: 1},
	} {
		t.Run(test.name, func(t *testing.T) {
			initial := newServingSetPublicationFixture(t, 7, 1, 0, publicationHandlers, publicationConsumers)
			state, engine := publicationConsumerState(t, initial)
			next := newServingSetPublicationFixture(t, test.revision, 2, test.ownership, publicationHandlers, publicationConsumers)
			activatePublicationFixture(t, state, engine, next)
			source := state.catalog
			state.catalog = func(ctx context.Context, installation, capability string) (ServingInstallation, ServingHandlerBinding, bool, error) {
				i, b, ok, err := source(ctx, installation, capability)
				if test.mutate != nil {
					test.mutate(&i, &b)
				}
				return i, b, ok, err
			}
			selections := append([]ServingSelection(nil), initial.selections...)
			for i := range selections {
				selections[i].SecurityEpoch = 2
			}
			if err := state.ReconcileAll(t.Context(), selections); err == nil || state.Ready() {
				t.Fatal("invalid publication was admitted")
			}
		})
	}
}

func TestPublicationRequiresValidOriginalConsumerDeployment(t *testing.T) {
	for _, mode := range []string{"missing original projection", "different original snapshot", "missing original receipt"} {
		t.Run(mode, func(t *testing.T) {
			consumers := publicationConsumers
			if mode == "missing original projection" {
				consumers = publicationConsumers[:2]
			}
			initial := newServingSetPublicationFixture(t, 7, 1, 0, publicationHandlers, consumers)
			state, engine := publicationConsumerState(t, initial)
			next := newServingSetPublicationFixture(t, 8, 2, 1, publicationHandlers, publicationConsumers)
			activatePublicationFixture(t, state, engine, next)
			switch mode {
			case "different original snapshot":
				state.consumers[0].DeploymentSnapshotDigest = catalogsnapshot.ComputeDigest([]byte("another deployment"))
			case "missing original receipt":
				delete(state.store.(*servingMemoryStore).artifacts, servingArtifactKey(catalogsnapshot.ArtifactServingCompatibilityReceipt, initial.selections[0].ExpectedCompatibilityReceiptDigest))
			}
			selections := append([]ServingSelection(nil), initial.selections...)
			for i := range selections {
				selections[i].SecurityEpoch = 2
			}
			if err := state.ReconcileAll(t.Context(), selections); err == nil || state.Ready() {
				t.Fatal("invalid original deployment hidden by valid active publication")
			}
		})
	}
}
