package runtimeapi

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

	"github.com/nexuscrm/kernel/pkg/apierr"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	delegation "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/delegationv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recyclebinv1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
)

type recycleBinListerStub struct {
	calls  int
	scope  contract.RecycleBinScope
	userID string
	result *contract.RecycleBinListResult
	err    error
	wait   bool
}

func (s *recycleBinListerStub) ListRecycleBin(ctx context.Context, user *models.UserSession, scope contract.RecycleBinScope) (*contract.RecycleBinListResult, error) {
	s.calls++
	s.scope = scope
	if user != nil {
		s.userID = user.ID
	}
	if s.wait {
		<-ctx.Done()
		return nil, ctx.Err()
	}
	return s.result, s.err
}

func recycleBinTestContext(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", 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)
	return context.WithValue(ctx, runtimeExecutionScopeContextKey{}, RuntimeExecutionScope{
		view: queryResponseTestView(t),
		receipts: semantic.Receipts{
			CatalogRevision: 1, SchemaGeneration: 2, EngineGeneration: 3, SecurityEpoch: 1,
		},
	})
}

func invokeRecycleBin(t *testing.T, ctx context.Context, server *RecycleBinServer, request semantic.Request) (semantic.Response, error) {
	t.Helper()
	wireRequest, err := mapping.ToWireListRecycleBinRequest(ctx, request)
	if err != nil {
		t.Fatal(err)
	}
	wireResponse, wireErr := server.ListRecycleBin(ctx, wireRequest)
	if wireErr != nil {
		return semantic.Response{}, mapping.FromRecycleBinStatusError(wireErr)
	}
	return mapping.FromWireListRecycleBinResponse(wireResponse)
}

func TestRecycleBinServerUsesTrustedUserAndPreservesCurrentProjection(t *testing.T) {
	lister := &recycleBinListerStub{result: &contract.RecycleBinListResult{
		Items: []contract.RecycleBinItem{{
			ID: "row-1", RecordID: "row-1", RecordName: "Visible", ObjectAPIName: "WorkItem",
			DeletedBy: "Test User", DeletedDate: "2026-07-22T04:00:00Z",
		}},
		FailedObjects: []contract.RecycleBinFailure{{
			ObjectAPIName: "RestrictedItem", ErrorCode: string(contract.RecycleBinFailureCodeForbidden), Message: "record read is forbidden",
		}},
	}}
	server, err := NewRecycleBinServer(lister, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	response, err := invokeRecycleBin(t, recycleBinTestContext(t), server, semantic.Request{Scope: semantic.ScopeAll})
	if err != nil {
		t.Fatal(err)
	}
	if lister.calls != 1 || lister.scope != contract.RecycleBinScopeAll || lister.userID != "user-recycle-bin" {
		t.Fatalf("lister calls=%d scope=%q user=%q", lister.calls, lister.scope, lister.userID)
	}
	if len(response.Items) != 1 || response.Items[0].RecordID != "row-1" || len(response.FailedObjects) != 1 ||
		response.FailedObjects[0].Code != semantic.ObjectFailureForbidden || response.Receipts.CapabilityVersion != semantic.CapabilityName ||
		response.Receipts.CatalogRevision != 1 {
		t.Fatalf("response = %#v", response)
	}
}

func TestRecycleBinServerRequiresBoundViewBeforeEngineAccess(t *testing.T) {
	lister := &recycleBinListerStub{}
	server, err := NewRecycleBinServer(lister, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := recycleBinTestContext(t)
	trusted, _ := trustedCallFromContext(ctx)
	base, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	ctx = context.WithValue(base, trustedCallContextKey{}, trusted)
	_, err = invokeRecycleBin(t, ctx, server, semantic.Request{Scope: semantic.ScopeMine})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureUnavailable || lister.calls != 0 {
		t.Fatalf("error=%#v calls=%d", err, lister.calls)
	}
}

func TestRecycleBinServerRejectsStaleRequestBeforeEngineAccess(t *testing.T) {
	lister := &recycleBinListerStub{}
	server, err := NewRecycleBinServer(lister, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	revision := uint64(2)
	_, err = invokeRecycleBin(t, recycleBinTestContext(t), server, semantic.Request{Scope: semantic.ScopeMine, MinCatalogRevision: &revision})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureStaleState || failure.ServedRevision != 1 || lister.calls != 0 {
		t.Fatalf("error=%#v calls=%d", err, lister.calls)
	}
}

func TestRecycleBinServerFailsClosedOnDuplicateProductIdentity(t *testing.T) {
	lister := &recycleBinListerStub{result: &contract.RecycleBinListResult{Items: []contract.RecycleBinItem{{
		ID: "row-1", RecordID: "row-other", ObjectAPIName: "WorkItem", DeletedDate: "2026-07-22T04:00:00Z",
	}}}}
	server, err := NewRecycleBinServer(lister, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	_, err = invokeRecycleBin(t, recycleBinTestContext(t), server, semantic.Request{Scope: semantic.ScopeMine})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureInternal {
		t.Fatalf("error = %#v", err)
	}
}

func TestRecycleBinServerCancellationAbortsWholeListing(t *testing.T) {
	lister := &recycleBinListerStub{wait: true}
	server, err := NewRecycleBinServer(lister, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(recycleBinTestContext(t), 20*time.Millisecond)
	defer cancel()
	_, err = invokeRecycleBin(t, ctx, server, semantic.Request{Scope: semantic.ScopeMine})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureDeadlineExceeded {
		t.Fatalf("error = %#v", err)
	}
}

func TestRecycleBinServerClassifiesScopeAllPermissionDenial(t *testing.T) {
	lister := &recycleBinListerStub{err: apierr.Forbidden("ViewAllData required")}
	server, err := NewRecycleBinServer(lister, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	_, err = invokeRecycleBin(t, recycleBinTestContext(t), server, semantic.Request{Scope: semantic.ScopeAll})
	var failure *semantic.Failure
	if !errors.As(err, &failure) || failure.Category != semantic.FailureForbidden {
		t.Fatalf("error = %#v", err)
	}
}
