package store

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/tech/sendico/gateway/tgsettle/storage/model"
	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

type fakePaymentsRepo struct {
	repository.Repository

	records              map[string]*model.PaymentRecord
	findErrByCall        map[int]error
	duplicateWhenZeroID  bool
	findCalls            int
	upsertCalls          int
	upsertIDs            []bson.ObjectID
	upsertIdempotencyKey []string
}

func (f *fakePaymentsRepo) FindOneByFilter(_ context.Context, query repository.FilterQuery, result storable.Storable) error {
	f.findCalls++
	if err, ok := f.findErrByCall[f.findCalls]; ok {
		return err
	}

	rec, ok := result.(*model.PaymentRecord)
	if !ok {
		return merrors.InvalidDataType("expected *model.PaymentRecord")
	}

	doc := query.BuildQuery()
	if key := stringField(doc, fieldIdempotencyKey); key != "" {
		stored, ok := f.records[key]
		if !ok {
			return merrors.NoData("payment not found by filter")
		}
		*rec = *stored
		return nil
	}
	if operationRef := stringField(doc, fieldOperationRef); operationRef != "" {
		for _, stored := range f.records {
			if strings.TrimSpace(stored.OperationRef) == operationRef {
				*rec = *stored
				return nil
			}
		}
		return merrors.NoData("payment not found by operation ref")
	}

	return merrors.NoData("payment not found")
}

func (f *fakePaymentsRepo) Upsert(_ context.Context, obj storable.Storable) error {
	f.upsertCalls++

	rec, ok := obj.(*model.PaymentRecord)
	if !ok {
		return merrors.InvalidDataType("expected *model.PaymentRecord")
	}
	f.upsertIDs = append(f.upsertIDs, rec.ID)
	f.upsertIdempotencyKey = append(f.upsertIdempotencyKey, rec.IdempotencyKey)

	if f.duplicateWhenZeroID && rec.ID.IsZero() {
		if _, exists := f.records[rec.IdempotencyKey]; exists {
			return mongo.WriteException{
				WriteErrors: mongo.WriteErrors{
					{
						Code:    11000,
						Message: "E11000 duplicate key error collection: tgsettle_gateway.payments",
					},
				},
			}
		}
	}

	copyRec := *rec
	if copyRec.ID.IsZero() {
		copyRec.ID = bson.NewObjectID()
	}
	if copyRec.CreatedAt.IsZero() {
		copyRec.CreatedAt = time.Now().UTC()
	}
	copyRec.UpdatedAt = time.Now().UTC()
	if f.records == nil {
		f.records = map[string]*model.PaymentRecord{}
	}
	f.records[copyRec.IdempotencyKey] = &copyRec
	*rec = copyRec
	return nil
}

func TestPaymentsUpsert_ReusesExistingIDFromIdempotencyLookup(t *testing.T) {
	key := "idem-existing"
	existingID := bson.NewObjectID()
	existingCreatedAt := time.Date(2026, 3, 6, 10, 0, 0, 0, time.UTC)

	repo := &fakePaymentsRepo{
		records: map[string]*model.PaymentRecord{
			key: {
				Base: storable.Base{
					ID:        existingID,
					CreatedAt: existingCreatedAt,
					UpdatedAt: existingCreatedAt,
				},
				IdempotencyKey: key,
				IntentRef:      "pi-old",
			},
		},
		duplicateWhenZeroID: true,
	}
	store := &Payments{logger: zap.NewNop(), repo: repo}

	record := &model.PaymentRecord{
		IdempotencyKey: key,
		IntentRef:      "pi-new",
		QuoteRef:       "quote-new",
	}

	if err := store.Upsert(context.Background(), record); err != nil {
		t.Fatalf("upsert failed: %v", err)
	}

	if repo.upsertCalls != 1 {
		t.Fatalf("expected one upsert call, got %d", repo.upsertCalls)
	}
	if len(repo.upsertIDs) != 1 || repo.upsertIDs[0] != existingID {
		t.Fatalf("expected upsert to reuse existing id %s, got %+v", existingID.Hex(), repo.upsertIDs)
	}
	if record.ID != existingID {
		t.Fatalf("record ID mismatch: got %s want %s", record.ID.Hex(), existingID.Hex())
	}
}

func TestPaymentsUpsert_RetriesAfterDuplicateKeyRace(t *testing.T) {
	key := "idem-race"
	existingID := bson.NewObjectID()

	repo := &fakePaymentsRepo{
		records: map[string]*model.PaymentRecord{
			key: {
				Base: storable.Base{
					ID:        existingID,
					CreatedAt: time.Date(2026, 3, 6, 10, 1, 0, 0, time.UTC),
					UpdatedAt: time.Date(2026, 3, 6, 10, 1, 0, 0, time.UTC),
				},
				IdempotencyKey: key,
				IntentRef:      "pi-existing",
			},
		},
		findErrByCall: map[int]error{
			1: merrors.NoData("payment not found by filter"),
		},
		duplicateWhenZeroID: true,
	}
	store := &Payments{logger: zap.NewNop(), repo: repo}

	record := &model.PaymentRecord{
		IdempotencyKey: key,
		IntentRef:      "pi-new",
		QuoteRef:       "quote-new",
	}

	if err := store.Upsert(context.Background(), record); err != nil {
		t.Fatalf("upsert failed: %v", err)
	}

	if repo.upsertCalls != 2 {
		t.Fatalf("expected two upsert calls, got %d", repo.upsertCalls)
	}
	if len(repo.upsertIDs) != 2 {
		t.Fatalf("expected two upsert IDs, got %d", len(repo.upsertIDs))
	}
	if !repo.upsertIDs[0].IsZero() {
		t.Fatalf("expected first upsert to use zero id due stale read, got %s", repo.upsertIDs[0].Hex())
	}
	if repo.upsertIDs[1] != existingID {
		t.Fatalf("expected retry to use existing id %s, got %s", existingID.Hex(), repo.upsertIDs[1].Hex())
	}
}

func TestPaymentsUpsert_PropagatesNoSuchTransactionAfterDuplicate(t *testing.T) {
	key := "idem-nosuchtx"

	repo := &fakePaymentsRepo{
		records: map[string]*model.PaymentRecord{
			key: {
				Base: storable.Base{
					ID:        bson.NewObjectID(),
					CreatedAt: time.Date(2026, 3, 6, 10, 2, 0, 0, time.UTC),
					UpdatedAt: time.Date(2026, 3, 6, 10, 2, 0, 0, time.UTC),
				},
				IdempotencyKey: key,
				IntentRef:      "pi-existing",
			},
		},
		findErrByCall: map[int]error{
			1: merrors.NoData("payment not found by filter"),
			2: mongo.CommandError{
				Code:    251,
				Name:    "NoSuchTransaction",
				Message: "Transaction with { txnNumber: 2 } has been aborted.",
			},
		},
		duplicateWhenZeroID: true,
	}
	store := &Payments{logger: zap.NewNop(), repo: repo}

	record := &model.PaymentRecord{
		IdempotencyKey: key,
		IntentRef:      "pi-new",
		QuoteRef:       "quote-new",
	}

	err := store.Upsert(context.Background(), record)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if !strings.Contains(err.Error(), "NoSuchTransaction") {
		t.Fatalf("expected NoSuchTransaction error, got %v", err)
	}
	if repo.upsertCalls != 1 {
		t.Fatalf("expected one upsert attempt before lookup failure, got %d", repo.upsertCalls)
	}
}

func stringField(doc bson.D, key string) string {
	for _, entry := range doc {
		if entry.Key != key {
			continue
		}
		res, _ := entry.Value.(string)
		return strings.TrimSpace(res)
	}
	return ""
}
