package quote_idempotency_service

import (
	"context"
	"errors"
	"testing"

	"github.com/tech/sendico/payments/storage/model"
	quotestorage "github.com/tech/sendico/payments/storage/quote"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestTryReuse_NotFound(t *testing.T) {
	svc := New()
	store := &fakeQuotesStore{
		getByIdempotencyKeyFn: func(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
			return nil, quotestorage.ErrQuoteNotFound
		},
	}

	record, reused, err := svc.TryReuse(context.Background(), store, ReuseInput{
		OrganizationID: bson.NewObjectID(),
		IdempotencyKey: "idem-1",
		Fingerprint:    "hash-1",
		Shape:          QuoteShapeSingle,
	})
	if err != nil {
		t.Fatalf("TryReuse returned error: %v", err)
	}
	if reused {
		t.Fatalf("expected reused=false")
	}
	if record != nil {
		t.Fatalf("expected nil record")
	}
}

func TestTryReuse_ParamMismatch(t *testing.T) {
	svc := New()
	store := &fakeQuotesStore{
		getByIdempotencyKeyFn: func(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
			return testSingleRecord("stored-hash", "q1"), nil
		},
	}

	_, _, err := svc.TryReuse(context.Background(), store, ReuseInput{
		OrganizationID: bson.NewObjectID(),
		IdempotencyKey: "idem-1",
		Fingerprint:    "different-hash",
		Shape:          QuoteShapeSingle,
	})
	if !errors.Is(err, ErrIdempotencyParamMismatch) {
		t.Fatalf("expected ErrIdempotencyParamMismatch, got %v", err)
	}
}

func TestTryReuse_ShapeMismatch(t *testing.T) {
	svc := New()
	store := &fakeQuotesStore{
		getByIdempotencyKeyFn: func(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
			return testSingleRecord("hash-1", "q1"), nil
		},
	}

	_, _, err := svc.TryReuse(context.Background(), store, ReuseInput{
		OrganizationID: bson.NewObjectID(),
		IdempotencyKey: "idem-1",
		Fingerprint:    "hash-1",
		Shape:          QuoteShapeBatch,
	})
	if !errors.Is(err, ErrIdempotencyShapeMismatch) {
		t.Fatalf("expected ErrIdempotencyShapeMismatch, got %v", err)
	}
}

func TestTryReuse_ShapeMismatchSingle(t *testing.T) {
	svc := New()
	store := &fakeQuotesStore{
		getByIdempotencyKeyFn: func(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
			return testBatchRecord("hash-1", "q1"), nil
		},
	}

	_, _, err := svc.TryReuse(context.Background(), store, ReuseInput{
		OrganizationID: bson.NewObjectID(),
		IdempotencyKey: "idem-1",
		Fingerprint:    "hash-1",
		Shape:          QuoteShapeSingle,
	})
	if !errors.Is(err, ErrIdempotencyShapeMismatch) {
		t.Fatalf("expected ErrIdempotencyShapeMismatch, got %v", err)
	}
}

func TestTryReuse_Success(t *testing.T) {
	svc := New()
	existing := testSingleRecord("hash-1", "q1")
	store := &fakeQuotesStore{
		getByIdempotencyKeyFn: func(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
			return existing, nil
		},
	}

	record, reused, err := svc.TryReuse(context.Background(), store, ReuseInput{
		OrganizationID: bson.NewObjectID(),
		IdempotencyKey: "idem-1",
		Fingerprint:    "hash-1",
		Shape:          QuoteShapeSingle,
	})
	if err != nil {
		t.Fatalf("TryReuse returned error: %v", err)
	}
	if !reused {
		t.Fatalf("expected reused=true")
	}
	if record != existing {
		t.Fatalf("expected existing record to be returned")
	}
}

func TestCreateOrReuse_CreateSuccess(t *testing.T) {
	svc := New()
	store := &fakeQuotesStore{
		createFn: func(context.Context, *model.PaymentQuoteRecord) error { return nil },
	}
	record := testSingleRecord("hash-1", "q1")

	got, reused, err := svc.CreateOrReuse(context.Background(), store, CreateInput{
		Record: record,
		Reuse: ReuseInput{
			OrganizationID: bson.NewObjectID(),
			IdempotencyKey: "idem-1",
			Fingerprint:    "hash-1",
			Shape:          QuoteShapeSingle,
		},
	})
	if err != nil {
		t.Fatalf("CreateOrReuse returned error: %v", err)
	}
	if reused {
		t.Fatalf("expected reused=false")
	}
	if got != record {
		t.Fatalf("expected newly created record")
	}
}

func TestCreateOrReuse_DuplicateReturnsExisting(t *testing.T) {
	svc := New()
	existing := testSingleRecord("hash-1", "q1")
	store := &fakeQuotesStore{
		createFn: func(context.Context, *model.PaymentQuoteRecord) error { return quotestorage.ErrDuplicateQuote },
		getByIdempotencyKeyFn: func(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
			return existing, nil
		},
	}
	record := testSingleRecord("hash-1", "q2")

	got, reused, err := svc.CreateOrReuse(context.Background(), store, CreateInput{
		Record: record,
		Reuse: ReuseInput{
			OrganizationID: bson.NewObjectID(),
			IdempotencyKey: "idem-1",
			Fingerprint:    "hash-1",
			Shape:          QuoteShapeSingle,
		},
	})
	if err != nil {
		t.Fatalf("CreateOrReuse returned error: %v", err)
	}
	if !reused {
		t.Fatalf("expected reused=true")
	}
	if got != existing {
		t.Fatalf("expected existing record")
	}
}

func TestCreateOrReuse_DuplicateParamMismatch(t *testing.T) {
	svc := New()
	store := &fakeQuotesStore{
		createFn: func(context.Context, *model.PaymentQuoteRecord) error { return quotestorage.ErrDuplicateQuote },
		getByIdempotencyKeyFn: func(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
			return testSingleRecord("stored-hash", "q1"), nil
		},
	}

	_, _, err := svc.CreateOrReuse(context.Background(), store, CreateInput{
		Record: testSingleRecord("new-hash", "q2"),
		Reuse: ReuseInput{
			OrganizationID: bson.NewObjectID(),
			IdempotencyKey: "idem-1",
			Fingerprint:    "new-hash",
			Shape:          QuoteShapeSingle,
		},
	})
	if !errors.Is(err, ErrIdempotencyParamMismatch) {
		t.Fatalf("expected ErrIdempotencyParamMismatch, got %v", err)
	}
}

func TestCreateOrReuse_DuplicateWithoutReusableRecordReturnsDuplicate(t *testing.T) {
	svc := New()
	store := &fakeQuotesStore{
		createFn: func(context.Context, *model.PaymentQuoteRecord) error { return quotestorage.ErrDuplicateQuote },
		getByIdempotencyKeyFn: func(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
			return nil, quotestorage.ErrQuoteNotFound
		},
	}

	_, _, err := svc.CreateOrReuse(context.Background(), store, CreateInput{
		Record: testSingleRecord("hash-1", "q2"),
		Reuse: ReuseInput{
			OrganizationID: bson.NewObjectID(),
			IdempotencyKey: "idem-1",
			Fingerprint:    "hash-1",
			Shape:          QuoteShapeSingle,
		},
	})
	if !errors.Is(err, quotestorage.ErrDuplicateQuote) {
		t.Fatalf("expected ErrDuplicateQuote, got %v", err)
	}
}

type fakeQuotesStore struct {
	createFn              func(ctx context.Context, quote *model.PaymentQuoteRecord) error
	getByIdempotencyKeyFn func(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.PaymentQuoteRecord, error)
}

func (f *fakeQuotesStore) Create(ctx context.Context, quote *model.PaymentQuoteRecord) error {
	if f.createFn == nil {
		return nil
	}
	return f.createFn(ctx, quote)
}

func (f *fakeQuotesStore) GetByRef(context.Context, bson.ObjectID, string) (*model.PaymentQuoteRecord, error) {
	return nil, quotestorage.ErrQuoteNotFound
}

func (f *fakeQuotesStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.PaymentQuoteRecord, error) {
	if f.getByIdempotencyKeyFn == nil {
		return nil, quotestorage.ErrQuoteNotFound
	}
	return f.getByIdempotencyKeyFn(ctx, orgRef, idempotencyKey)
}

func testSingleRecord(hash, quoteRef string) *model.PaymentQuoteRecord {
	return &model.PaymentQuoteRecord{
		IdempotencyKey: "idem-1",
		Hash:           hash,
		RequestShape:   model.QuoteRequestShapeSingle,
		Items: []*model.PaymentQuoteItemV2{
			{
				Intent: &model.PaymentIntent{Ref: "intent-1"},
				Quote:  &model.PaymentQuoteSnapshot{QuoteRef: quoteRef},
				Status: &model.QuoteStatusV2{State: model.QuoteStateExecutable},
			},
		},
	}
}

func testBatchRecord(hash, quoteRef string) *model.PaymentQuoteRecord {
	return &model.PaymentQuoteRecord{
		IdempotencyKey: "idem-1",
		Hash:           hash,
		RequestShape:   model.QuoteRequestShapeBatch,
		Items: []*model.PaymentQuoteItemV2{
			{
				Intent: &model.PaymentIntent{Ref: "intent-1"},
				Quote:  &model.PaymentQuoteSnapshot{QuoteRef: quoteRef},
				Status: &model.QuoteStatusV2{State: model.QuoteStateExecutable},
			},
		},
	}
}
