package gateway

import (
	"context"
	"encoding/json"
	"sync"
	"testing"

	"github.com/tech/sendico/gateway/tgsettle/storage"
	storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
	envelope "github.com/tech/sendico/pkg/messaging/envelope"
	mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
	"github.com/tech/sendico/pkg/model"
	notification "github.com/tech/sendico/pkg/model/notification"
	"github.com/tech/sendico/pkg/mservice"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
)

type fakePaymentsStore struct {
	mu      sync.Mutex
	records map[string]*storagemodel.PaymentRecord
}

func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string) (*storagemodel.PaymentRecord, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.records[key], nil
}

func (f *fakePaymentsStore) Upsert(_ context.Context, record *storagemodel.PaymentRecord) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.records == nil {
		f.records = map[string]*storagemodel.PaymentRecord{}
	}
	f.records[record.IdempotencyKey] = record
	return nil
}

type fakeTelegramStore struct {
	mu      sync.Mutex
	records map[string]*storagemodel.TelegramConfirmation
}

func (f *fakeTelegramStore) Upsert(_ context.Context, record *storagemodel.TelegramConfirmation) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.records == nil {
		f.records = map[string]*storagemodel.TelegramConfirmation{}
	}
	f.records[record.RequestID] = record
	return nil
}

type fakeRepo struct {
	payments *fakePaymentsStore
	tg       *fakeTelegramStore
}

func (f *fakeRepo) Payments() storage.PaymentsStore {
	return f.payments
}

func (f *fakeRepo) TelegramConfirmations() storage.TelegramConfirmationsStore {
	return f.tg
}

type captureProducer struct {
	mu                   sync.Mutex
	confirmationRequests []*model.ConfirmationRequest
	executions           []*model.PaymentGatewayExecution
}

func (c *captureProducer) SendMessage(env envelope.Envelope) error {
	_, _ = env.Serialize()
	switch env.GetSignature().ToString() {
	case model.NewNotification(mservice.Notifications, notification.NAConfirmationRequest).ToString():
		var req model.ConfirmationRequest
		if err := json.Unmarshal(env.GetData(), &req); err == nil {
			c.mu.Lock()
			c.confirmationRequests = append(c.confirmationRequests, &req)
			c.mu.Unlock()
		}
	case model.NewNotification(mservice.PaymentGateway, notification.NAPaymentGatewayExecution).ToString():
		var exec model.PaymentGatewayExecution
		if err := json.Unmarshal(env.GetData(), &exec); err == nil {
			c.mu.Lock()
			c.executions = append(c.executions, &exec)
			c.mu.Unlock()
		}
	}
	return nil
}

func (c *captureProducer) Reset() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.confirmationRequests = nil
	c.executions = nil
}

func TestOnIntentCreatesConfirmationRequest(t *testing.T) {
	logger := mloggerfactory.NewLogger(false)
	repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
	prod := &captureProducer{}
	t.Setenv("PGS_CHAT_ID", "-100")
	svc := NewService(logger, repo, prod, nil, Config{
		Rail:            "card",
		TargetChatIDEnv: "PGS_CHAT_ID",
		TimeoutSeconds:  90,
		AcceptedUserIDs: []string{"42"},
	})
	prod.Reset()

	intent := &model.PaymentGatewayIntent{
		PaymentIntentID: "pi-1",
		IdempotencyKey:  "idem-1",
		OutgoingLeg:     "card",
		QuoteRef:        "quote-1",
		RequestedMoney:  &paymenttypes.Money{Amount: "10.50", Currency: "USD"},
		TargetChatID:    "",
	}
	if err := svc.onIntent(context.Background(), intent); err != nil {
		t.Fatalf("onIntent error: %v", err)
	}
	if len(prod.confirmationRequests) != 1 {
		t.Fatalf("expected 1 confirmation request, got %d", len(prod.confirmationRequests))
	}
	req := prod.confirmationRequests[0]
	if req.RequestID != "idem-1" || req.PaymentIntentID != "pi-1" || req.QuoteRef != "quote-1" {
		t.Fatalf("unexpected confirmation request fields: %#v", req)
	}
	if req.TargetChatID != "-100" {
		t.Fatalf("expected target chat id -100, got %q", req.TargetChatID)
	}
	if req.RequestedMoney == nil || req.RequestedMoney.Amount != "10.50" || req.RequestedMoney.Currency != "USD" {
		t.Fatalf("requested money mismatch: %#v", req.RequestedMoney)
	}
	if req.TimeoutSeconds != 90 {
		t.Fatalf("expected timeout 90, got %d", req.TimeoutSeconds)
	}
	if req.SourceService != string(mservice.PaymentGateway) || req.Rail != "card" {
		t.Fatalf("unexpected source/rail: %#v", req)
	}
	record := repo.payments.records["idem-1"]
	if record == nil {
		t.Fatalf("expected pending payment to be stored")
	}
	if record.Status != storagemodel.PaymentStatusPending {
		t.Fatalf("expected pending status, got %q", record.Status)
	}
	if record.RequestedMoney == nil || record.RequestedMoney.Amount != "10.50" {
		t.Fatalf("requested money not stored correctly: %#v", record.RequestedMoney)
	}
}

func TestIntentFromSubmitTransferUsesSourceMoney(t *testing.T) {
	req := &chainv1.SubmitTransferRequest{
		IdempotencyKey:  "idem-1",
		ClientReference: "pi-1",
		Amount:          &moneyv1.Money{Amount: "10.00", Currency: "EUR"},
		Metadata: map[string]string{
			metadataSourceAmount:   "12.34",
			metadataSourceCurrency: "USD",
		},
	}
	intent, err := intentFromSubmitTransfer(req, "provider_settlement", "")
	if err != nil {
		t.Fatalf("intentFromSubmitTransfer error: %v", err)
	}
	if intent.RequestedMoney == nil || intent.RequestedMoney.Amount != "12.34" || intent.RequestedMoney.Currency != "USD" {
		t.Fatalf("expected source money override, got: %#v", intent.RequestedMoney)
	}
}

func TestConfirmationResultPersistsExecutionAndReply(t *testing.T) {
	logger := mloggerfactory.NewLogger(false)
	repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
	prod := &captureProducer{}
	svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
	intent := &model.PaymentGatewayIntent{
		PaymentIntentID: "pi-2",
		IdempotencyKey:  "idem-2",
		QuoteRef:        "quote-2",
		OutgoingLeg:     "card",
		RequestedMoney:  &paymenttypes.Money{Amount: "5", Currency: "EUR"},
	}
	_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
		IdempotencyKey:  "idem-2",
		PaymentIntentID: intent.PaymentIntentID,
		QuoteRef:        intent.QuoteRef,
		OutgoingLeg:     intent.OutgoingLeg,
		RequestedMoney:  intent.RequestedMoney,
		Status:          storagemodel.PaymentStatusPending,
	})

	result := &model.ConfirmationResult{
		RequestID: "idem-2",
		Money:     &paymenttypes.Money{Amount: "5", Currency: "EUR"},
		Status:    model.ConfirmationStatusConfirmed,
		RawReply:  &model.TelegramMessage{ChatID: "1", MessageID: "2", Text: "5 EUR"},
	}
	if err := svc.onConfirmationResult(context.Background(), result); err != nil {
		t.Fatalf("onConfirmationResult error: %v", err)
	}
	record := repo.payments.records["idem-2"]
	if record == nil {
		t.Fatalf("expected payment record to be stored")
	}
	if record.Status != storagemodel.PaymentStatusExecuted {
		t.Fatalf("expected executed status, got %q", record.Status)
	}
	if record.ExecutedMoney == nil || record.ExecutedMoney.Amount != "5" {
		t.Fatalf("executed money not stored correctly: %#v", record.ExecutedMoney)
	}
	if repo.tg.records["idem-2"] == nil || repo.tg.records["idem-2"].RawReply.Text != "5 EUR" {
		t.Fatalf("telegram reply not stored correctly")
	}
}

func TestClarifiedResultPersistsExecution(t *testing.T) {
	logger := mloggerfactory.NewLogger(false)
	repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
	prod := &captureProducer{}
	svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
	intent := &model.PaymentGatewayIntent{
		PaymentIntentID: "pi-clarified",
		IdempotencyKey:  "idem-clarified",
		QuoteRef:        "quote-clarified",
		OutgoingLeg:     "card",
		RequestedMoney:  &paymenttypes.Money{Amount: "12", Currency: "USD"},
	}
	_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
		IdempotencyKey:  "idem-clarified",
		PaymentIntentID: intent.PaymentIntentID,
		QuoteRef:        intent.QuoteRef,
		OutgoingLeg:     intent.OutgoingLeg,
		RequestedMoney:  intent.RequestedMoney,
		Status:          storagemodel.PaymentStatusPending,
	})

	result := &model.ConfirmationResult{
		RequestID: "idem-clarified",
		Money:     &paymenttypes.Money{Amount: "12", Currency: "USD"},
		Status:    model.ConfirmationStatusClarified,
		RawReply:  &model.TelegramMessage{ChatID: "1", MessageID: "3", Text: "12 USD"},
	}
	if err := svc.onConfirmationResult(context.Background(), result); err != nil {
		t.Fatalf("onConfirmationResult error: %v", err)
	}
	record := repo.payments.records["idem-clarified"]
	if record == nil || record.Status != storagemodel.PaymentStatusExecuted {
		t.Fatalf("expected payment executed status, got %#v", record)
	}
}

func TestIdempotencyPreventsDuplicateWrites(t *testing.T) {
	logger := mloggerfactory.NewLogger(false)
	repo := &fakeRepo{payments: &fakePaymentsStore{records: map[string]*storagemodel.PaymentRecord{
		"idem-3": {IdempotencyKey: "idem-3", Status: storagemodel.PaymentStatusPending},
	}}, tg: &fakeTelegramStore{}}
	prod := &captureProducer{}
	svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
	intent := &model.PaymentGatewayIntent{
		PaymentIntentID: "pi-3",
		IdempotencyKey:  "idem-3",
		OutgoingLeg:     "card",
		QuoteRef:        "quote-3",
		RequestedMoney:  &paymenttypes.Money{Amount: "1", Currency: "USD"},
		TargetChatID:    "chat",
	}
	if err := svc.onIntent(context.Background(), intent); err != nil {
		t.Fatalf("onIntent error: %v", err)
	}
	if len(prod.confirmationRequests) != 0 {
		t.Fatalf("expected no confirmation request for duplicate intent")
	}
}

func TestTimeoutDoesNotPersistExecution(t *testing.T) {
	logger := mloggerfactory.NewLogger(false)
	repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
	prod := &captureProducer{}
	svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
	intent := &model.PaymentGatewayIntent{
		PaymentIntentID: "pi-4",
		IdempotencyKey:  "idem-4",
		QuoteRef:        "quote-4",
		OutgoingLeg:     "card",
		RequestedMoney:  &paymenttypes.Money{Amount: "8", Currency: "USD"},
	}
	_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
		IdempotencyKey:  "idem-4",
		PaymentIntentID: intent.PaymentIntentID,
		QuoteRef:        intent.QuoteRef,
		OutgoingLeg:     intent.OutgoingLeg,
		RequestedMoney:  intent.RequestedMoney,
		Status:          storagemodel.PaymentStatusPending,
	})

	result := &model.ConfirmationResult{
		RequestID: "idem-4",
		Status:    model.ConfirmationStatusTimeout,
	}
	if err := svc.onConfirmationResult(context.Background(), result); err != nil {
		t.Fatalf("onConfirmationResult error: %v", err)
	}
	record := repo.payments.records["idem-4"]
	if record == nil || record.Status != storagemodel.PaymentStatusExpired {
		t.Fatalf("expected expired status for timeout, got %#v", record)
	}
}

func TestRejectedDoesNotPersistExecution(t *testing.T) {
	logger := mloggerfactory.NewLogger(false)
	repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
	prod := &captureProducer{}
	svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
	intent := &model.PaymentGatewayIntent{
		PaymentIntentID: "pi-reject",
		IdempotencyKey:  "idem-reject",
		QuoteRef:        "quote-reject",
		OutgoingLeg:     "card",
		RequestedMoney:  &paymenttypes.Money{Amount: "3", Currency: "USD"},
	}
	_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
		IdempotencyKey:  "idem-reject",
		PaymentIntentID: intent.PaymentIntentID,
		QuoteRef:        intent.QuoteRef,
		OutgoingLeg:     intent.OutgoingLeg,
		RequestedMoney:  intent.RequestedMoney,
		Status:          storagemodel.PaymentStatusPending,
	})

	result := &model.ConfirmationResult{
		RequestID: "idem-reject",
		Status:    model.ConfirmationStatusRejected,
		RawReply:  &model.TelegramMessage{ChatID: "1", MessageID: "4", Text: "no"},
	}
	if err := svc.onConfirmationResult(context.Background(), result); err != nil {
		t.Fatalf("onConfirmationResult error: %v", err)
	}
	record := repo.payments.records["idem-reject"]
	if record == nil || record.Status != storagemodel.PaymentStatusExpired {
		t.Fatalf("expected expired status for rejection, got %#v", record)
	}
	if repo.tg.records["idem-reject"] == nil {
		t.Fatalf("expected raw reply to be stored for rejection")
	}
}
