package runtimeapi

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/apierr"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/shared/pkg/models"
	delegation "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/delegationv1"
	mutationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recyclebinmutationv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
)

type recycleBinMutatorStub struct {
	restoreCalls int
	purgeCalls   int
	recordID     string
	userID       string
	err          error
}

func (s *recycleBinMutatorStub) RestoreRecycleBinRecord(_ context.Context, recordID string, user *models.UserSession) error {
	s.restoreCalls++
	s.capture(recordID, user)
	return s.err
}

func (s *recycleBinMutatorStub) PurgeRecycleBinRecord(_ context.Context, recordID string, user *models.UserSession) error {
	s.purgeCalls++
	s.capture(recordID, user)
	return s.err
}

func (s *recycleBinMutatorStub) capture(recordID string, user *models.UserSession) {
	s.recordID = recordID
	if user != nil {
		s.userID = user.ID
	}
}

func recycleBinMutationTestContext(t *testing.T) context.Context {
	t.Helper()
	base, cancel := context.WithTimeout(context.Background(), time.Second)
	t.Cleanup(cancel)
	trusted := newTrustedCall(delegation.Claims{
		DelegationID: "delegation-recycle-bin-mutation", OrgID: "org-recycle-bin", PrincipalID: "user-recycle-bin",
		SessionID: "session-recycle-bin", CallerWorkloadID: "appservice-recycle-bin",
		AppInstallationID: "installation-recycle-bin", RuntimeAudience: "runtime-recycle-bin",
		Capabilities: []string{semantic.CapabilityName}, AuthEpoch: 1, SecurityEpoch: 1,
	}, models.UserSession{ID: "user-recycle-bin"})
	ctx := context.WithValue(base, trustedCallContextKey{}, trusted)
	ctx = context.WithValue(ctx, runtimeExecutionScopeContextKey{}, RuntimeExecutionScope{
		view: queryResponseTestView(t),
		receipts: semantic.Receipts{
			CatalogRevision: 1, SchemaGeneration: 2, EngineGeneration: 3, SecurityEpoch: 1,
		},
	})
	_, mutationArtifact := servingArtifacts(t, 1, mutationsemantic.CapabilityName, catalogsnapshot.ProjectionRecordMutationV1)
	mutationView, err := catalogsnapshot.OpenRuntimeMetadataView(mutationArtifact)
	if err != nil {
		t.Fatal(err)
	}
	return context.WithValue(ctx, runtimeMutationMetadataViewContextKey{}, mutationView)
}

func invokeRecycleBinRestore(t *testing.T, ctx context.Context, server *RecycleBinMutationServer, request semantic.Request) (semantic.Response, error) {
	t.Helper()
	wireRequest, err := mapping.ToWireRestoreRecycleBinRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	wireResponse, wireErr := server.RestoreRecycleBin(ctx, wireRequest)
	if wireErr != nil {
		return semantic.Response{}, mapping.FromRecycleBinMutationStatusError(wireErr)
	}
	return mapping.FromWireRestoreRecycleBinResponse(request, wireResponse)
}

func invokeRecycleBinPurge(t *testing.T, ctx context.Context, server *RecycleBinMutationServer, request semantic.Request) (semantic.Response, error) {
	t.Helper()
	wireRequest, err := mapping.ToWirePurgeRecycleBinRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	wireResponse, wireErr := server.PurgeRecycleBin(ctx, wireRequest)
	if wireErr != nil {
		return semantic.Response{}, mapping.FromRecycleBinMutationStatusError(wireErr)
	}
	return mapping.FromWirePurgeRecycleBinResponse(request, wireResponse)
}

func TestRecycleBinMutationServerUsesOneExistingEngineForRestoreAndPurge(t *testing.T) {
	mutator := &recycleBinMutatorStub{}
	server, err := NewRecycleBinMutationServer(mutator, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := recycleBinMutationTestContext(t)
	restore, err := invokeRecycleBinRestore(t, ctx, server, semantic.Request{RecordID: "row-1"})
	if err != nil {
		t.Fatal(err)
	}
	purge, err := invokeRecycleBinPurge(t, ctx, server, semantic.Request{RecordID: "row-2"})
	if err != nil {
		t.Fatal(err)
	}
	if mutator.restoreCalls != 1 || mutator.purgeCalls != 1 || mutator.recordID != "row-2" || mutator.userID != "user-recycle-bin" {
		t.Fatalf("calls=%d/%d record=%q user=%q", mutator.restoreCalls, mutator.purgeCalls, mutator.recordID, mutator.userID)
	}
	for name, response := range map[string]semantic.Response{"restore": restore, "purge": purge} {
		if response.Receipts.CapabilityVersion != semantic.CapabilityName || response.Receipts.CatalogRevision != 1 {
			t.Fatalf("%s response=%#v", name, response)
		}
	}
}

func TestRecycleBinMutationServerRejectsStaleStateBeforeMutation(t *testing.T) {
	mutator := &recycleBinMutatorStub{}
	server, err := NewRecycleBinMutationServer(mutator, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	revision := uint64(2)
	_, err = invokeRecycleBinRestore(t, recycleBinMutationTestContext(t), server, semantic.Request{RecordID: "row-1", MinCatalogRevision: &revision})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureStaleState || failure.ServedRevision != 1 || mutator.restoreCalls != 0 {
		t.Fatalf("error=%#v calls=%d", err, mutator.restoreCalls)
	}
}

func TestRecycleBinMutationServerRejectsMissingMutationExecutionAuthority(t *testing.T) {
	mutator := &recycleBinMutatorStub{}
	server, err := NewRecycleBinMutationServer(mutator, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := recycleBinMutationTestContext(t)
	ctx = context.WithValue(ctx, runtimeMutationMetadataViewContextKey{}, (*catalogsnapshot.RuntimeMetadataView)(nil))
	_, err = invokeRecycleBinRestore(t, ctx, server, semantic.Request{RecordID: "row-1"})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureUnavailable || mutator.restoreCalls != 0 {
		t.Fatalf("error=%#v calls=%d", err, mutator.restoreCalls)
	}
}

func TestRecycleBinMutationServerPreservesClosedEngineOutcomeGrammar(t *testing.T) {
	for _, test := range []struct {
		name string
		err  error
		want semantic.FailureCategory
	}{
		{name: "invalid", err: apierr.Invalid("invalid record"), want: semantic.FailureInvalidRequest},
		{name: "forbidden", err: apierr.Forbidden("denied"), want: semantic.FailureForbidden},
		{name: "missing", err: apierr.NotFound("record not found"), want: semantic.FailureMissing},
		{name: "conflict", err: apierr.Conflict("record changed during restore"), want: semantic.FailureConflict},
		{name: "live-context engine cancellation", err: context.Canceled, want: semantic.FailureUnavailable},
		{name: "opaque", err: errors.New("opaque engine failure"), want: semantic.FailureInternal},
	} {
		t.Run(test.name, func(t *testing.T) {
			mutator := &recycleBinMutatorStub{err: test.err}
			server, err := NewRecycleBinMutationServer(mutator, nil, nil)
			if err != nil {
				t.Fatal(err)
			}
			_, err = invokeRecycleBinRestore(t, recycleBinMutationTestContext(t), server, semantic.Request{RecordID: "row-1"})
			var failure *semantic.Failure
			if !errors.As(err, &failure) || failure.Category != test.want || mutator.restoreCalls != 1 {
				t.Fatalf("error=%#v calls=%d, want %s", err, mutator.restoreCalls, test.want)
			}
		})
	}
}

func TestRecycleBinMutationServerAuthenticatesBeforeDecoding(t *testing.T) {
	mutator := &recycleBinMutatorStub{}
	server, err := NewRecycleBinMutationServer(mutator, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	_, wireErr := server.RestoreRecycleBin(context.Background(), &runtimev1.RestoreRecycleBinRequest{})
	var failure *semantic.Failure
	err = mapping.FromRecycleBinMutationStatusError(wireErr)
	if !errors.As(err, &failure) || failure.Category != semantic.FailureUnauthenticated || mutator.restoreCalls != 0 {
		t.Fatalf("error=%#v calls=%d", err, mutator.restoreCalls)
	}
}

func TestRecycleBinMutationServerInternalOwnerWinsOverWrappedConflict(t *testing.T) {
	mutator := &recycleBinMutatorStub{err: appErrors.NewInternalError("storage", apierr.Conflict("wrapped conflict"))}
	server, err := NewRecycleBinMutationServer(mutator, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	_, err = invokeRecycleBinRestore(t, recycleBinMutationTestContext(t), server, semantic.Request{RecordID: "row-1"})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureInternal {
		t.Fatalf("error=%#v", err)
	}
}

var _ runtimev1.RecycleBinMutationServiceServer = (*RecycleBinMutationServer)(nil)
