package packs

import (
	"context"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

func TestExternalLegacyManagedSetFailsClosedBeforeCallbacksOrComponents(t *testing.T) {
	events := []string{}
	registration := testComponentKindRegistration(cp.ComponentKindUIAction, "StandardAction")
	registration.Contract.Phase = cp.InstallPlanPhaseAutomation
	registration.ManagedProvider.Preflight = func(_ context.Context, _ cp.ComponentRowReader, request cp.ComponentManagedRequest) error {
		events = append(events, "preflight:"+request.Component.BusinessKey)
		return nil
	}
	registration.ManagedProvider.Apply = func(_ context.Context, _ cp.ComponentMutationStore, request cp.ComponentManagedRequest) error {
		events = append(events, "apply:"+request.Component.BusinessKey)
		return nil
	}
	var listRequest, reconcileRequest cp.ComponentManagedSetRequest
	var reconciled []string
	registration.ManagedProvider.ListLiveBusinessKeys = func(_ context.Context, _ cp.ComponentRowReader, request cp.ComponentManagedSetRequest) ([]cp.ComponentLiveBusinessKey, error) {
		listRequest = request
		events = append(events, "list")
		return []cp.ComponentLiveBusinessKey{
			{BusinessKey: "Case/One"}, {BusinessKey: "Case/Two"},
			{BusinessKey: "Case/Customized", Customized: true}, {BusinessKey: "Case/Stale"},
		}, nil
	}
	registration.ManagedProvider.ReconcileSet = func(_ context.Context, _ cp.ComponentMutationStore, request cp.ComponentManagedSetRequest, keys []string) error {
		reconcileRequest, reconciled = request, append([]string(nil), keys...)
		events = append(events, "reconcile")
		return nil
	}
	component := func(key string) *sourceBackedExternalManagedComponent {
		return newSourceBackedExternalManagedComponent("test-pack", ManifestComponent{
			Kind: ComponentKindUIAction, BusinessKey: key, Manageability: ManageabilityLocked, Payload: []byte(`{}`),
		}, []byte(`{}`), cp.ComponentBuildManagedComponent{ExpectedProjection: []byte(`{}`)}, registration)
	}
	setComponent, err := newSourceBackedExternalManagedSetComponent("test-pack", registration, []string{"Case/Two", "Case/One"})
	if err != nil {
		t.Fatal(err)
	}
	plan, err := NewInstallPlan([]PackComponent{
		component("Case/Two"), component("Case/One"),
		&fakePlanComponent{kind: ComponentKindStandardFlow, key: "legacy-sweeper", applied: &events},
		setComponent,
	})
	if err != nil {
		t.Fatal(err)
	}
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = db.Close() })
	if err := plan.Apply(context.Background(), ApplyDeps{DB: db}, InstallPlanHooks{AfterPhase: func(context.Context, InstallPlanPhase) error {
		events = append(events, "hook")
		return nil
	}}); err == nil || !strings.Contains(err.Error(), "legacy managed provider execution is disabled") {
		t.Fatalf("Apply() error = %v", err)
	}
	if len(events) != 0 {
		t.Fatalf("callbacks/components ran before fail-closed barrier: %v", events)
	}
	if listRequest != (cp.ComponentManagedSetRequest{}) || reconcileRequest != (cp.ComponentManagedSetRequest{}) {
		t.Fatalf("deprecated callbacks received requests: %#v/%#v", listRequest, reconcileRequest)
	}
	if reconciled != nil {
		t.Fatalf("deprecated reconcile keys = %v, want nil", reconciled)
	}
	if managedComponentSetNeedsReconcile([]string{"Case/One"}, []cp.ComponentLiveBusinessKey{{BusinessKey: "Case/Customized", Customized: true}}) {
		t.Fatal("customized-only live set requested reconciliation")
	}
	if componentInstallApplyGroupKey(setComponent) == componentInstallApplyGroupKey(component("@managed-set")) {
		t.Fatal("managed set reconciliation shares an apply group with a provider business key")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func TestExternalManagedSetRequiresDeclaredKindAndSourceOwnerFailsClosed(t *testing.T) {
	source := registeredManagedRouteTestSource()
	source.Manifest.Components = nil
	registration := testComponentKindRegistration(cp.ComponentKindApprovalProcess, "ProcessDefinition")
	registration.ManagedProvider.ListLiveBusinessKeys = func(context.Context, cp.ComponentRowReader, cp.ComponentManagedSetRequest) ([]cp.ComponentLiveBusinessKey, error) {
		return []cp.ComponentLiveBusinessKey{{BusinessKey: "Case/Dropped"}}, nil
	}
	var reconciled []string
	reconcileCalled := false
	registration.ManagedProvider.ReconcileSet = func(_ context.Context, _ cp.ComponentMutationStore, _ cp.ComponentManagedSetRequest, keys []string) error {
		reconcileCalled = true
		reconciled = append([]string(nil), keys...)
		return nil
	}
	pack, err := NewSourceBackedPack(source, WithComponentKindRegistrations([]cp.ComponentKindRegistration{registration}))
	if err != nil {
		t.Fatal(err)
	}
	declared, err := DeclaredComponentsForPack(pack)
	if err != nil {
		t.Fatal(err)
	}
	plan, err := pack.InstallPlan()
	if err != nil {
		t.Fatal(err)
	}
	if len(pack.Components()) != 0 || len(declared) != 0 {
		t.Fatalf("components/declared = %d/%d", len(pack.Components()), len(declared))
	}
	var setComponent *sourceBackedExternalManagedSetComponent
	for _, component := range plan.ComponentsForPhase(InstallPlanPhase(registration.Contract.Phase)) {
		if managedSet, ok := component.(*sourceBackedExternalManagedSetComponent); ok {
			setComponent = managedSet
		}
	}
	if setComponent != nil {
		t.Fatal("source without a declared component kind acquired authoritative empty-set reconciliation")
	}
	if reconcileCalled || reconciled != nil {
		t.Fatalf("undeclared source called/keys = %v/%#v", reconcileCalled, reconciled)
	}

	source.Manifest.Components = []ManifestComponent{{
		Kind: ComponentKindApprovalProcess, BusinessKey: "Case/Declared", Manageability: ManageabilityLocked, Payload: []byte(`{}`),
	}}
	registration.BuildHandler = func(buildContext cp.ComponentBuildContext, _ cp.ComponentBuildRequest) error {
		return buildContext.AddManagedComponent(cp.ComponentBuildManagedComponent{ExpectedProjection: []byte(`{}`)})
	}
	registration.Contract.OwnerDiscriminator = cp.ComponentOwnerStrategySource
	sourceOwnedPack, err := NewSourceBackedPack(source, WithComponentKindRegistrations([]cp.ComponentKindRegistration{registration}))
	if err != nil {
		t.Fatal(err)
	}
	if _, err := sourceOwnedPack.InstallPlan(); err == nil || !strings.Contains(err.Error(), "has no explicit source owner") {
		t.Fatalf("source-owned set error = %v", err)
	}
}

func TestExternalManagedSetVerificationDetectsExactSetDrift(t *testing.T) {
	source := registeredManagedRouteTestSource()
	source.Manifest.Components = []ManifestComponent{{
		Kind: ComponentKindApprovalProcess, BusinessKey: "Case/Declared", Manageability: ManageabilityLocked, Payload: []byte(`{}`),
	}}
	base := testComponentKindRegistration(cp.ComponentKindApprovalProcess, "ProcessDefinition")
	base.ManagedProvider = nil
	base.BuildHandler = func(buildContext cp.ComponentBuildContext, _ cp.ComponentBuildRequest) error {
		return buildContext.AddManagedComponent(cp.ComponentBuildManagedComponent{ExpectedProjection: []byte(`{}`)})
	}
	original := testComponentManagedLifecycleRegistration(t, cp.ComponentKindApprovalProcess, "ProcessDefinition")
	provider := original.Lifecycle()
	setProjectCalls := 0
	provider.Project = func(_ context.Context, scope cp.ComponentManagedScope, request cp.ComponentManagedLifecycleRequest, _ cp.ComponentManagedReadStore) (cp.ComponentManagedProjection, error) {
		if scope == cp.ComponentManagedScopeItem {
			return cp.ComponentManagedProjection{Identity: []byte("item"), Expected: []byte("same"), Live: []byte("same")}, nil
		}
		setProjectCalls++
		if scope != cp.ComponentManagedScopeSet || request.Set == nil || len(request.DesiredBusinessKeys) != 1 || request.DesiredBusinessKeys[0] != "Case/Declared" {
			t.Fatalf("set verification request = scope %q, request %#v", scope, request)
		}
		return cp.ComponentManagedProjection{Identity: []byte("set"), Expected: []byte("empty"), Live: []byte("stale")}, nil
	}
	lifecycle, err := cp.NewComponentManagedLifecycleRegistration(cp.ComponentManagedLifecycleRegistrationSpec{
		Kind: original.Kind(), ReadSet: original.ReadSet(),
		Capabilities: append(original.Capabilities(), cp.ComponentManagedCapabilityExactSet), EffectClasses: original.EffectClasses(),
		TransactionMode: original.TransactionMode(), Budget: original.Budget(), Lifecycle: provider,
	})
	if err != nil {
		t.Fatal(err)
	}
	bound, err := base.WithManagedLifecycle(lifecycle)
	if err != nil {
		t.Fatal(err)
	}
	pack, err := NewSourceBackedPack(source, WithComponentKindManagedLifecycles(nil, []cp.ComponentKindManagedLifecycleRegistration{bound}))
	if err != nil {
		t.Fatal(err)
	}
	pack.sweepComponents = nil
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = db.Close() })
	mock.ExpectBegin()
	mock.ExpectRollback()
	mock.ExpectBegin()
	mock.ExpectRollback()
	if err := pack.Verify(context.Background(), VerifyDeps{DB: db}); err == nil || !strings.Contains(err.Error(), "projection drift") {
		t.Fatalf("Verify() error = %v", err)
	}
	if setProjectCalls != 1 {
		t.Fatalf("set projection calls = %d, want 1", setProjectCalls)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}
