package services

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

	"github.com/nexuscrm/analytics"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/contract"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
)

func TestRollupMaintenanceRequiresServingPreconditionBeforeMutation(t *testing.T) {
	service := &ServiceManager{Rollup: &analytics.RollupService{}}
	_, err := service.RecalculateRollups(
		context.Background(),
		contract.RollupRecalculateRequest{Object: "Quote"},
	)
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) ||
		rollupErr.Kind != contract.RollupMaintenanceErrorPreconditionRequired {
		t.Fatalf("rollup maintenance error = %#v, want precondition_required", err)
	}
}

func TestRollupMaintenanceTreatsSuppliedWhitespaceAsAPreconditionValue(t *testing.T) {
	var got string
	service := &ServiceManager{
		Rollup: &analytics.RollupService{},
		deps: InfrastructureDeps{
			RollupMaintenanceFence: func(
				_ context.Context,
				expected string,
				_ func(context.Context) error,
			) RollupMaintenanceFenceResult {
				got = expected
				return RollupMaintenanceFenceResult{OperationErr: &contract.RollupMaintenanceError{
					Kind:    contract.RollupMaintenanceErrorPreconditionFailed,
					Message: "serving attestation does not match the current serving state",
				}}
			},
		},
	}
	_, err := service.RecalculateRollups(
		context.Background(),
		contract.RollupRecalculateRequest{
			Object:                     "Quote",
			ExpectedServingAttestation: "   ",
		},
	)
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) ||
		rollupErr.Kind != contract.RollupMaintenanceErrorPreconditionFailed {
		t.Fatalf("rollup maintenance error = %#v, want precondition_failed", err)
	}
	if got != "   " {
		t.Fatalf("serving attestation = %q, want supplied whitespace unchanged", got)
	}
}

func TestRollupMaintenanceRejectsNegativeMaxParentsBeforeReachingTheFence(t *testing.T) {
	service := &ServiceManager{Rollup: &analytics.RollupService{}}
	_, err := service.RecalculateRollups(
		context.Background(),
		contract.RollupRecalculateRequest{
			Object:                     "Quote",
			ExpectedServingAttestation: "serving-attestation",
			MaxParents:                 -1,
		},
	)
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) ||
		rollupErr.Kind != contract.RollupMaintenanceErrorInvalidRequest {
		// The fence is unwired on this ServiceManager, so an "unavailable" here
		// would mean the malformed bound was never inspected at all.
		t.Fatalf("rollup maintenance error = %#v, want invalid_request", err)
	}
}

// TestRollupRecalculateDeadlineIsNotYetEnabled pins the staging decision, not a
// number. Bounding the server while the pinned dialect still emits only
// {object, recalculated, status} would make a truncated scan indistinguishable
// on the wire from a completed one. The bound turns on in the change that pins
// a dialect able to report Complete; until then this must stay unbounded.
func TestRollupRecalculateDeadlineIsNotYetEnabled(t *testing.T) {
	if !rollupRecalculateDeadline().IsZero() {
		t.Fatalf("rollupRecalculateDeadline() = %v, want the zero instant until a dialect can report a partial scan",
			rollupRecalculateDeadline())
	}
	bounds := rollupRecalculateBounds(contract.RollupRecalculateRequest{Object: "Quote"}, rollupRecalculateDeadline())
	if !bounds.Deadline.IsZero() {
		t.Fatalf("bounds.Deadline = %v, want unbounded", bounds.Deadline)
	}
}

// TestRollupRecalculateBoundsCarryTheRequestThrough covers the request -> bounds
// hop. Without it, dropping any field from the mapping leaves every test green
// while the endpoint silently ignores the caller's cursor or count.
func TestRollupRecalculateBoundsCarryTheRequestThrough(t *testing.T) {
	deadline := time.Unix(1, 0).UTC()
	bounds := rollupRecalculateBounds(contract.RollupRecalculateRequest{
		Object:     "Quote",
		AfterID:    "  parent-042  ",
		MaxParents: 25,
	}, deadline)

	if bounds.AfterID != "parent-042" {
		t.Fatalf("bounds.AfterID = %q, want the trimmed request cursor", bounds.AfterID)
	}
	if bounds.MaxParents != 25 {
		t.Fatalf("bounds.MaxParents = %d, want 25 from the request", bounds.MaxParents)
	}
	if !bounds.Deadline.Equal(deadline) {
		t.Fatalf("bounds.Deadline = %v, want %v", bounds.Deadline, deadline)
	}
}

// TestRollupRecalculateResultCarriesProgressThrough covers the progress ->
// result hop. Without it, dropping NextAfterID or Complete leaves every test
// green while the feature becomes inert over the wire.
func TestRollupRecalculateResultCarriesProgressThrough(t *testing.T) {
	// Both polarities of Complete are exercised on purpose. Asserting only the
	// false case would pass against a mapping that dropped the field entirely,
	// because false is also the zero value.
	for _, tc := range []struct {
		name     string
		progress ports.RollupRecalculateProgress
	}{
		{"partial", ports.RollupRecalculateProgress{Recalculated: 18, NextAfterID: "parent-060", Complete: false}},
		{"complete", ports.RollupRecalculateProgress{Recalculated: 4, NextAfterID: "", Complete: true}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			result := rollupRecalculateResult("Quote", tc.progress)
			if result.Object != "Quote" {
				t.Fatalf("result.Object = %q, want Quote", result.Object)
			}
			if result.Recalculated != tc.progress.Recalculated {
				t.Fatalf("result.Recalculated = %d, want %d", result.Recalculated, tc.progress.Recalculated)
			}
			if result.NextAfterID != tc.progress.NextAfterID {
				t.Fatalf("result.NextAfterID = %q, want %q", result.NextAfterID, tc.progress.NextAfterID)
			}
			if result.Complete != tc.progress.Complete {
				t.Fatalf("result.Complete = %t, want the engine's %t", result.Complete, tc.progress.Complete)
			}
		})
	}
}

func TestRollupMaintenanceFenceResultPreservesOperationClassification(t *testing.T) {
	cause := appErrors.NewNotFoundError("Object", "Bogus")
	err := rollupMaintenanceFenceResultError(RollupMaintenanceFenceResult{
		OperationErr: cause,
	})
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) || rollupErr.Kind != contract.RollupMaintenanceErrorNotFound {
		t.Fatalf("operation error = %#v, want not_found", err)
	}
	if !errors.Is(err, cause) {
		t.Fatal("operation cause was not preserved across the serving fence")
	}
}

func TestRollupMaintenanceFenceResultMapsOnlyFenceFailureToUnavailable(t *testing.T) {
	operationErr := appErrors.NewNotFoundError("Object", "Bogus")
	fenceErr := errors.New("apply-lock lease lost")
	err := rollupMaintenanceFenceResultError(RollupMaintenanceFenceResult{
		OperationErr: operationErr,
		FenceErr:     fenceErr,
	})
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) || rollupErr.Kind != contract.RollupMaintenanceErrorUnavailable {
		t.Fatalf("fence error = %#v, want unavailable", err)
	}
	if !errors.Is(err, operationErr) || !errors.Is(err, fenceErr) {
		t.Fatal("combined operation and fence causes were not preserved")
	}
}

func TestRollupMaintenanceErrorClassifiesNotFound(t *testing.T) {
	err := rollupMaintenanceError(appErrors.NewNotFoundError("Object", "Bogus"))
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorNotFound {
		t.Fatalf("kind = %q, want not_found", rollupErr.Kind)
	}
	if rollupErr.Message != "Object with ID 'Bogus' not found" {
		t.Fatalf("message = %q", rollupErr.Message)
	}
}

func TestRollupMaintenanceErrorClassifiesValidationFields(t *testing.T) {
	err := rollupMaintenanceError(appErrors.NewValidationError("Filter, Other", "invalid filter"))
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorValidation {
		t.Fatalf("kind = %q, want validation", rollupErr.Kind)
	}
	if rollupErr.Message != "invalid filter" {
		t.Fatalf("message = %q", rollupErr.Message)
	}
	if len(rollupErr.Fields) != 2 || rollupErr.Fields[0] != "Filter" || rollupErr.Fields[1] != "Other" {
		t.Fatalf("fields = %#v, want Filter/Other", rollupErr.Fields)
	}
}

func TestRollupMaintenanceErrorClassifiesValidationRule(t *testing.T) {
	err := rollupMaintenanceError(appErrors.NewValidationRuleError("Rollup_Profile_Gate", "Description is required"))
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorValidationRule {
		t.Fatalf("kind = %q, want validation_rule", rollupErr.Kind)
	}
	if rollupErr.Message != "Description is required" {
		t.Fatalf("message = %q", rollupErr.Message)
	}
}

func TestRollupMaintenanceErrorPreservesAllValidationRuleFailures(t *testing.T) {
	err := rollupMaintenanceError(appErrors.NewValidationRuleFailures([]appErrors.ValidationRuleFailure{
		{RuleName: "IQM_Existing_Database", Message: "Existing Database is required", Field: "Existing_Database__c"},
		{RuleName: "IQM_Completed_Date", Message: "IQM Completed Date is required", Field: "IQM_Completed_Date__c"},
	}))
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorValidationRule {
		t.Fatalf("kind = %q, want validation_rule", rollupErr.Kind)
	}
	if rollupErr.Message != "Existing Database is required; IQM Completed Date is required" {
		t.Fatalf("message = %q", rollupErr.Message)
	}
	if len(rollupErr.Fields) != 2 || rollupErr.Fields[0] != "Existing_Database__c" || rollupErr.Fields[1] != "IQM_Completed_Date__c" {
		t.Fatalf("fields = %#v, want both validation display fields", rollupErr.Fields)
	}
}

func TestRollupMaintenanceErrorPreservesMixedStaticAndRuleValidationDetails(t *testing.T) {
	cause := appErrors.NewClientValidationErrors(
		appErrors.NewValidationError("LastName", "Last name is required"),
		appErrors.NewValidationError("Company", "Company is required"),
		appErrors.NewValidationRuleFailures([]appErrors.ValidationRuleFailure{{
			RuleName: "Industry_Required", Message: "Industry is required", Field: "Industry",
		}}),
	)
	err := rollupMaintenanceError(cause)
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorValidation {
		t.Fatalf("kind = %q, want validation for mixed static/rule rejection", rollupErr.Kind)
	}
	if rollupErr.Message != "Last name is required; Company is required; Industry is required" {
		t.Fatalf("message = %q, want every ordered validation diagnostic", rollupErr.Message)
	}
	wantFields := []string{"LastName", "Company", "Industry"}
	if !reflect.DeepEqual(rollupErr.Fields, wantFields) {
		t.Fatalf("fields = %#v, want %#v", rollupErr.Fields, wantFields)
	}
}

func TestRollupMaintenanceErrorClassifiesFLSDenied(t *testing.T) {
	err := rollupMaintenanceError(appErrors.NewFLSDeniedErrorForFields([]string{"Amount", "StageName"}))
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorFieldDenied {
		t.Fatalf("kind = %q, want field_denied", rollupErr.Kind)
	}
	if len(rollupErr.Fields) != 2 || rollupErr.Fields[0] != "Amount" || rollupErr.Fields[1] != "StageName" {
		t.Fatalf("fields = %#v, want Amount/StageName", rollupErr.Fields)
	}
}

func TestRollupMaintenanceErrorPreservesDomainWire(t *testing.T) {
	cause := sharedErrors.NewInvalidReferenceError("bad parent reference", "ParentId")
	err := rollupMaintenanceError(cause)
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorDomainWire {
		t.Fatalf("kind = %q, want domain_wire", rollupErr.Kind)
	}
	if !errors.Is(rollupErr, cause) {
		t.Fatal("domain wire cause was not preserved across rollup seam")
	}
	if len(rollupErr.Fields) != 1 || rollupErr.Fields[0] != "ParentId" {
		t.Fatalf("fields = %#v, want ParentId", rollupErr.Fields)
	}
}

func TestRollupMaintenanceErrorInternalStillBeatsWrappedDomainWire(t *testing.T) {
	cause := appErrors.NewInternalError("rollback failed", sharedErrors.NewFieldIntegrityError("hidden field detail", "Amount"))
	err := rollupMaintenanceError(cause)
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorInternal {
		t.Fatalf("kind = %q, want internal", rollupErr.Kind)
	}
	if rollupErr.Message != "An internal server error occurred. Contact your system administrator." {
		t.Fatalf("message leaked internal detail: %q", rollupErr.Message)
	}
}

func TestRollupMaintenanceErrorClassifiesLockContention(t *testing.T) {
	err := rollupMaintenanceError(appErrors.NewLockContentionError(errors.New("1205 lock wait timeout")))
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorLockContention {
		t.Fatalf("kind = %q, want lock_contention", rollupErr.Kind)
	}
}

func TestRollupMaintenanceErrorClassifiesPermission(t *testing.T) {
	err := rollupMaintenanceError(appErrors.NewPermissionError("read", "Account"))
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorPermission {
		t.Fatalf("kind = %q, want permission", rollupErr.Kind)
	}
}

func TestRollupMaintenanceErrorSanitizesInternal(t *testing.T) {
	err := rollupMaintenanceError(appErrors.NewInternalError("sql: SELECT secret FROM table", nil))
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		t.Fatalf("error = %T %[1]v, want RollupMaintenanceError", err)
	}
	if rollupErr.Kind != contract.RollupMaintenanceErrorInternal {
		t.Fatalf("kind = %q, want internal", rollupErr.Kind)
	}
	if rollupErr.Message != "An internal server error occurred. Contact your system administrator." {
		t.Fatalf("message leaked internal detail: %q", rollupErr.Message)
	}
}
