package batch_quote_processor_v2

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/tech/sendico/payments/quotation/internal/service/quotation/transfer_intent_hydrator"
	"github.com/tech/sendico/pkg/merrors"
	quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestProcess_DerivesPerItemIdempotency(t *testing.T) {
	processor := &fakeSingleProcessor{}
	svc := New(processor)
	orgID := bson.NewObjectID()

	out, err := svc.Process(context.Background(), ProcessInput{
		Context: BatchContext{
			OrganizationRef:    orgID.Hex(),
			OrganizationID:     orgID,
			InitiatorRef:       "initiator-1",
			PreviewOnly:        false,
			BaseIdempotencyKey: "idem-batch",
		},
		Intents: []*transfer_intent_hydrator.QuoteIntent{
			{Ref: "i1"},
			{Ref: "i2"},
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if out == nil || len(out.Items) != 2 {
		t.Fatalf("expected two items")
	}

	if got, want := processor.calls[0].Item.IdempotencyKey, "idem-batch:1"; got != want {
		t.Fatalf("unexpected first item key: got=%q want=%q", got, want)
	}
	if got, want := processor.calls[1].Item.IdempotencyKey, "idem-batch:2"; got != want {
		t.Fatalf("unexpected second item key: got=%q want=%q", got, want)
	}
	if out.Items[0].Item.Index != 0 || out.Items[1].Item.Index != 1 {
		t.Fatalf("expected stable ordered indices")
	}
}

func TestProcess_SingleIntentKeepsBaseIdempotency(t *testing.T) {
	processor := &fakeSingleProcessor{}
	svc := New(processor)
	orgID := bson.NewObjectID()

	_, err := svc.Process(context.Background(), ProcessInput{
		Context: BatchContext{
			OrganizationRef:    orgID.Hex(),
			OrganizationID:     orgID,
			InitiatorRef:       "initiator-1",
			PreviewOnly:        false,
			BaseIdempotencyKey: "idem-single",
		},
		Intents: []*transfer_intent_hydrator.QuoteIntent{
			{Ref: "i1"},
		},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got, want := processor.calls[0].Item.IdempotencyKey, "idem-single"; got != want {
		t.Fatalf("unexpected idempotency key: got=%q want=%q", got, want)
	}
}

func TestProcess_PreviewUsesEmptyPerItemIdempotency(t *testing.T) {
	processor := &fakeSingleProcessor{}
	svc := New(processor)
	orgID := bson.NewObjectID()

	_, err := svc.Process(context.Background(), ProcessInput{
		Context: BatchContext{
			OrganizationRef: orgID.Hex(),
			OrganizationID:  orgID,
			InitiatorRef:    "initiator-1",
			PreviewOnly:     true,
		},
		Intents: []*transfer_intent_hydrator.QuoteIntent{{Ref: "i1"}, {Ref: "i2"}},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if processor.calls[0].Item.IdempotencyKey != "" || processor.calls[1].Item.IdempotencyKey != "" {
		t.Fatalf("expected empty idempotency keys for preview")
	}
}

func TestProcess_WrapsItemError(t *testing.T) {
	processor := &fakeSingleProcessor{
		errAtIndex: 1,
		err:        errors.New("boom"),
	}
	svc := New(processor)
	orgID := bson.NewObjectID()

	_, err := svc.Process(context.Background(), ProcessInput{
		Context: BatchContext{
			OrganizationRef:    orgID.Hex(),
			OrganizationID:     orgID,
			InitiatorRef:       "initiator-1",
			PreviewOnly:        false,
			BaseIdempotencyKey: "idem",
		},
		Intents: []*transfer_intent_hydrator.QuoteIntent{{Ref: "i1"}, {Ref: "i2"}},
	})
	if err == nil {
		t.Fatalf("expected wrapped error")
	}
	if !strings.Contains(err.Error(), "intents[1]") {
		t.Fatalf("expected indexed error, got %v", err)
	}
}

func TestProcess_Validation(t *testing.T) {
	orgID := bson.NewObjectID()
	baseInput := ProcessInput{
		Context: BatchContext{
			OrganizationRef:    orgID.Hex(),
			OrganizationID:     orgID,
			InitiatorRef:       "initiator-1",
			PreviewOnly:        false,
			BaseIdempotencyKey: "idem",
		},
		Intents: []*transfer_intent_hydrator.QuoteIntent{{Ref: "i1"}},
	}

	_, err := New(nil).Process(context.Background(), baseInput)
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for nil single processor, got %v", err)
	}

	previewWithIdem := baseInput
	previewWithIdem.Context.PreviewOnly = true
	_, err = New(&fakeSingleProcessor{}).Process(context.Background(), previewWithIdem)
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for preview idempotency, got %v", err)
	}

	nonPreviewNoIdem := baseInput
	nonPreviewNoIdem.Context.BaseIdempotencyKey = ""
	_, err = New(&fakeSingleProcessor{}).Process(context.Background(), nonPreviewNoIdem)
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for missing idempotency, got %v", err)
	}

	withNilIntent := baseInput
	withNilIntent.Intents = []*transfer_intent_hydrator.QuoteIntent{nil}
	_, err = New(&fakeSingleProcessor{}).Process(context.Background(), withNilIntent)
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for nil intent, got %v", err)
	}
}

func TestProcess_RejectsNilQuoteOutput(t *testing.T) {
	svc := New(&fakeSingleProcessor{returnNilQuote: true})
	orgID := bson.NewObjectID()

	_, err := svc.Process(context.Background(), ProcessInput{
		Context: BatchContext{
			OrganizationRef:    orgID.Hex(),
			OrganizationID:     orgID,
			InitiatorRef:       "initiator-1",
			PreviewOnly:        false,
			BaseIdempotencyKey: "idem",
		},
		Intents: []*transfer_intent_hydrator.QuoteIntent{{Ref: "i1"}},
	})
	if !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument for nil quote output, got %v", err)
	}
}

type fakeSingleProcessor struct {
	calls          []SingleProcessInput
	errAtIndex     int
	err            error
	returnNilQuote bool
}

func (f *fakeSingleProcessor) Process(_ context.Context, in SingleProcessInput) (*SingleProcessOutput, error) {
	f.calls = append(f.calls, in)
	if f.err != nil && in.Item.Index == f.errAtIndex {
		return nil, f.err
	}
	if f.returnNilQuote {
		return &SingleProcessOutput{}, nil
	}
	return &SingleProcessOutput{
		Quote: &quotationv2.PaymentQuote{QuoteRef: in.Item.Intent.Ref},
	}, nil
}
