package store

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

	"github.com/tech/sendico/fx/storage"
	"github.com/tech/sendico/fx/storage/model"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	"go.uber.org/zap"
)

func TestQuotesStoreIssue(t *testing.T) {
	ctx := context.Background()
	var inserted *model.Quote
	repo := &repoStub{
		insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error {
			inserted = cloneQuote(t, obj)
			return nil
		},
	}
	store := &quotesStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}}

	quote := &model.Quote{QuoteRef: "q1"}
	if err := store.Issue(ctx, quote); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if inserted == nil || inserted.Status != model.QuoteStatusIssued {
		t.Fatalf("expected issued quote to be inserted")
	}
	if inserted.PricedAtUnixMs <= 0 {
		t.Fatalf("expected pricedAtUnixMs to be populated")
	}
}

func TestQuotesStoreIssueSetsExpiryDate(t *testing.T) {
	ctx := context.Background()
	var inserted *model.Quote
	repo := &repoStub{
		insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error {
			inserted = cloneQuote(t, obj)
			return nil
		},
	}
	store := &quotesStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}}

	expiry := time.Now().Add(2 * time.Minute).UnixMilli()
	quote := &model.Quote{
		QuoteRef:        "q1",
		ExpiresAtUnixMs: expiry,
	}

	if err := store.Issue(ctx, quote); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if inserted == nil || inserted.ExpiresAt == nil {
		t.Fatalf("expected expiry timestamp to be populated")
	}
	if inserted.ExpiresAt.UnixMilli() != expiry {
		t.Fatalf("expected expiry to equal %d, got %d", expiry, inserted.ExpiresAt.UnixMilli())
	}
}

func TestQuotesStoreIssueInvalidInput(t *testing.T) {
	store := &quotesStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}}
	if err := store.Issue(context.Background(), nil); !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument error, got %v", err)
	}
}

func TestQuotesStoreConsumeSuccess(t *testing.T) {
	ctx := context.Background()
	now := time.Now()
	ledgerRef := "ledger-1"

	stored := &model.Quote{
		QuoteRef:        "q1",
		Firm:            true,
		Status:          model.QuoteStatusIssued,
		ExpiresAtUnixMs: now.Add(5 * time.Minute).UnixMilli(),
	}
	var updated *model.Quote
	repo := &repoStub{
		findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error {
			quote := result.(*model.Quote)
			*quote = *stored
			return nil
		},
		updateFn: func(_ context.Context, obj storable.Storable) error {
			updated = cloneQuote(t, obj)
			return nil
		},
	}
	factory := &txFactoryStub{}
	store := &quotesStore{logger: zap.NewNop(), repo: repo, txFactory: factory}

	res, err := store.Consume(ctx, "q1", ledgerRef, now)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if res == nil || res.Status != model.QuoteStatusConsumed {
		t.Fatalf("expected consumed quote")
	}
	if updated == nil || updated.ConsumedByLedgerTxnRef != ledgerRef {
		t.Fatalf("expected update with ledger ref")
	}
}

func TestQuotesStoreConsumeExpired(t *testing.T) {
	ctx := context.Background()
	stored := &model.Quote{
		QuoteRef:        "q1",
		Firm:            true,
		Status:          model.QuoteStatusIssued,
		ExpiresAtUnixMs: time.Now().Add(-time.Minute).UnixMilli(),
	}
	var updated *model.Quote
	repo := &repoStub{
		findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error {
			quote := result.(*model.Quote)
			*quote = *stored
			return nil
		},
		updateFn: func(_ context.Context, obj storable.Storable) error {
			updated = cloneQuote(t, obj)
			return nil
		},
	}
	factory := &txFactoryStub{}
	store := &quotesStore{logger: zap.NewNop(), repo: repo, txFactory: factory}

	_, err := store.Consume(ctx, "q1", "ledger", time.Now())
	if !errors.Is(err, storage.ErrQuoteExpired) {
		t.Fatalf("expected ErrQuoteExpired, got %v", err)
	}
	if updated == nil || updated.Status != model.QuoteStatusExpired {
		t.Fatalf("expected quote marked expired")
	}
}

func TestQuotesStoreExpireIssuedBefore(t *testing.T) {
	repo := &repoStub{
		patchManyFn: func(context.Context, builder.Query, builder.Patch) (int, error) {
			return 3, nil
		},
	}
	store := &quotesStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}}

	count, err := store.ExpireIssuedBefore(context.Background(), time.Now())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if count != 3 {
		t.Fatalf("expected 3 expired quotes, got %d", count)
	}
}

func TestQuotesStoreExpireZeroCutoff(t *testing.T) {
	store := &quotesStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}}
	if _, err := store.ExpireIssuedBefore(context.Background(), time.Time{}); !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument error")
	}
}

func TestQuotesStoreGetByRefNotFound(t *testing.T) {
	repo := &repoStub{
		findOneFn: func(context.Context, builder.Query, storable.Storable) error {
			return merrors.ErrNoData
		},
	}
	store := &quotesStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}}

	if _, err := store.GetByRef(context.Background(), "missing"); !errors.Is(err, merrors.ErrNoData) {
		t.Fatalf("expected ErrNoData, got %v", err)
	}
}

func TestQuotesStoreGetByRefInvalid(t *testing.T) {
	store := &quotesStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}}
	if _, err := store.GetByRef(context.Background(), ""); !errors.Is(err, merrors.ErrInvalidArg) {
		t.Fatalf("expected invalid argument error")
	}
}
