package gateway

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/tech/sendico/gateway/chsettle/storage"
	storagemodel "github.com/tech/sendico/gateway/chsettle/storage/model"
	"github.com/tech/sendico/pkg/discovery"
	envelope "github.com/tech/sendico/pkg/messaging/envelope"
	tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram"
	mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
	"github.com/tech/sendico/pkg/model"
	"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"
)

//
// FAKE STORES
//

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()
	if f.records == nil {
		return nil, nil //nolint:nilnil // fake store: no records means no payment by idempotency key
	}
	return f.records[key], nil
}

func (f *fakePaymentsStore) FindByOperationRef(_ context.Context, key string) (*storagemodel.PaymentRecord, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.records == nil {
		return nil, nil //nolint:nilnil // fake store: no records means no payment by operation ref
	}
	for _, record := range f.records {
		if record != nil && record.OperationRef == key {
			return record, nil
		}
	}
	return nil, nil //nolint:nilnil // fake store: operation ref not found
}

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
	pending  *fakePendingStore
	treasury storage.TreasuryRequestsStore
	users    storage.TreasuryTelegramUsersStore
}

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

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

func (f *fakeRepo) PendingConfirmations() storage.PendingConfirmationsStore {
	return f.pending
}

func (f *fakeRepo) TreasuryRequests() storage.TreasuryRequestsStore {
	return f.treasury
}

func (f *fakeRepo) TreasuryTelegramUsers() storage.TreasuryTelegramUsersStore {
	return f.users
}

type fakePendingStore struct {
	mu      sync.Mutex
	records map[string]*storagemodel.PendingConfirmation
}

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

func (f *fakePendingStore) FindByRequestID(_ context.Context, requestID string) (*storagemodel.PendingConfirmation, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.records == nil {
		return nil, nil //nolint:nilnil // fake store: no pending confirmations configured
	}
	return f.records[requestID], nil
}

func (f *fakePendingStore) FindByMessageID(_ context.Context, messageID string) (*storagemodel.PendingConfirmation, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	for _, record := range f.records {
		if record != nil && record.MessageID == messageID {
			return record, nil
		}
	}
	return nil, nil //nolint:nilnil // fake store: message id not found
}

func (f *fakePendingStore) MarkClarified(_ context.Context, requestID string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if record := f.records[requestID]; record != nil {
		record.Clarified = true
	}
	return nil
}

func (f *fakePendingStore) AttachMessage(_ context.Context, requestID string, messageID string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if record := f.records[requestID]; record != nil {
		if record.MessageID == "" {
			record.MessageID = messageID
		}
	}
	return nil
}

func (f *fakePendingStore) DeleteByRequestID(_ context.Context, requestID string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	delete(f.records, requestID)
	return nil
}

func (f *fakePendingStore) ListExpired(_ context.Context, now time.Time, limit int64) ([]storagemodel.PendingConfirmation, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if limit <= 0 {
		limit = 100
	}
	result := make([]storagemodel.PendingConfirmation, 0)
	for _, record := range f.records {
		if record == nil || record.ExpiresAt.IsZero() || record.ExpiresAt.After(now) {
			continue
		}
		result = append(result, *record)
		if int64(len(result)) >= limit {
			break
		}
	}
	return result, nil
}

//
// FAKE BROKER (ОБЯЗАТЕЛЕН ДЛЯ СЕРВИСА)
//

type fakeBroker struct{}

func (f *fakeBroker) Publish(_ envelope.Envelope) error {
	return nil
}

func (f *fakeBroker) Subscribe(event model.NotificationEvent) (<-chan envelope.Envelope, error) {
	return nil, nil //nolint:nilnil // fake broker does not emit events in unit tests
}

func (f *fakeBroker) Unsubscribe(event model.NotificationEvent, subChan <-chan envelope.Envelope) error {
	return nil
}

//
// CAPTURE ONLY TELEGRAM REACTIONS
//

type captureProducer struct {
	mu        sync.Mutex
	reactions []envelope.Envelope
	sig       string
}

func (c *captureProducer) SendMessage(env envelope.Envelope) error {
	if env.GetSignature().ToString() != c.sig {
		return nil
	}
	c.mu.Lock()
	c.reactions = append(c.reactions, env)
	c.mu.Unlock()
	return nil
}

//
// TESTS
//

func newTestService(_ *testing.T) (*Service, *fakeRepo, *captureProducer) {
	logger := mloggerfactory.NewLogger(false)

	repo := &fakeRepo{
		payments: &fakePaymentsStore{},
		tg:       &fakeTelegramStore{},
		pending:  &fakePendingStore{},
	}

	sigEnv := tnotifications.TelegramReaction(mservice.PaymentGateway, &model.TelegramReactionRequest{
		RequestID: "x",
		ChatID:    "1",
		MessageID: "2",
		Emoji:     "ok",
	})

	prod := &captureProducer{
		sig: sigEnv.GetSignature().ToString(),
	}

	svc := NewService(logger, repo, prod, &fakeBroker{}, Config{
		Rail:            "card",
		SuccessReaction: "👍",
	})

	return svc, repo, prod
}

func TestConfirmed(t *testing.T) {
	svc, repo, prod := newTestService(t)

	_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
		IdempotencyKey:  "idem-1",
		PaymentIntentID: "pi-1",
		QuoteRef:        "quote-1",
		OutgoingLeg:     "card",
		RequestedMoney:  &paymenttypes.Money{Amount: "5", Currency: "EUR"},
		Status:          storagemodel.PaymentStatusWaiting,
	})

	result := &model.ConfirmationResult{
		RequestID: "idem-1",
		Money:     &paymenttypes.Money{Amount: "5", Currency: "EUR"},
		Status:    model.ConfirmationStatusConfirmed,
		RawReply:  &model.TelegramMessage{ChatID: "1", MessageID: "2"},
	}

	_ = svc.onConfirmationResult(context.Background(), result)

	rec := repo.payments.records["idem-1"]

	if rec.Status != storagemodel.PaymentStatusSuccess {
		t.Fatalf("expected success, got %s", rec.Status)
	}
	if rec.RequestedMoney == nil {
		t.Fatalf("requested money not set")
	}
	if rec.ExecutedAt.IsZero() {
		t.Fatalf("executedAt not set")
	}
	if repo.tg.records["idem-1"] == nil {
		t.Fatalf("telegram confirmation not stored")
	}
	if len(prod.reactions) != 1 {
		t.Fatalf("reaction must be published")
	}
}

func TestClarified(t *testing.T) {
	svc, repo, prod := newTestService(t)

	_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
		IdempotencyKey: "idem-2",
		Status:         storagemodel.PaymentStatusWaiting,
	})

	result := &model.ConfirmationResult{
		RequestID: "idem-2",
		Status:    model.ConfirmationStatusClarified,
		RawReply:  &model.TelegramMessage{ChatID: "1", MessageID: "2"},
	}

	_ = svc.onConfirmationResult(context.Background(), result)

	rec := repo.payments.records["idem-2"]

	if rec.Status != storagemodel.PaymentStatusWaiting {
		t.Fatalf("clarified must not change status")
	}
	if repo.tg.records["idem-2"] == nil {
		t.Fatalf("telegram confirmation must be stored")
	}
	if len(prod.reactions) != 0 {
		t.Fatalf("clarified must not publish reaction")
	}
}

func TestRejected(t *testing.T) {
	svc, repo, prod := newTestService(t)

	// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
	// даем минимально ожидаемые поля + non-nil ExecutedMoney.
	_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
		IdempotencyKey:  "idem-3",
		PaymentIntentID: "pi-3",
		QuoteRef:        "quote-3",
		OutgoingLeg:     "card",
		RequestedMoney:  &paymenttypes.Money{Amount: "5", Currency: "EUR"},
		ExecutedMoney:   &paymenttypes.Money{Amount: "0", Currency: "EUR"},
		Status:          storagemodel.PaymentStatusWaiting,
	})

	result := &model.ConfirmationResult{
		RequestID: "idem-3",
		Status:    model.ConfirmationStatusRejected,
		RawReply:  &model.TelegramMessage{ChatID: "1", MessageID: "2"},
	}

	_ = svc.onConfirmationResult(context.Background(), result)

	rec := repo.payments.records["idem-3"]

	if rec.Status != storagemodel.PaymentStatusFailed {
		t.Fatalf("expected failed")
	}
	if repo.tg.records["idem-3"] == nil {
		t.Fatalf("telegram confirmation must be stored")
	}
	if len(prod.reactions) != 0 {
		t.Fatalf("rejected must not publish reaction")
	}
}

func TestTimeout(t *testing.T) {
	svc, repo, prod := newTestService(t)

	// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
	// даем минимально ожидаемые поля + non-nil ExecutedMoney.
	_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
		IdempotencyKey:  "idem-4",
		PaymentIntentID: "pi-4",
		QuoteRef:        "quote-4",
		OutgoingLeg:     "card",
		RequestedMoney:  &paymenttypes.Money{Amount: "5", Currency: "EUR"},
		ExecutedMoney:   &paymenttypes.Money{Amount: "0", Currency: "EUR"},
		Status:          storagemodel.PaymentStatusWaiting,
	})

	result := &model.ConfirmationResult{
		RequestID: "idem-4",
		Status:    model.ConfirmationStatusTimeout,
		RawReply:  &model.TelegramMessage{ChatID: "1", MessageID: "2"},
	}

	_ = svc.onConfirmationResult(context.Background(), result)

	rec := repo.payments.records["idem-4"]

	if rec.Status != storagemodel.PaymentStatusFailed {
		t.Fatalf("timeout must be failed")
	}
	if repo.tg.records["idem-4"] == nil {
		t.Fatalf("telegram confirmation must be stored")
	}
	if len(prod.reactions) != 0 {
		t.Fatalf("timeout must not publish reaction")
	}
}

func TestIntentFromSubmitTransfer_NormalizesOutgoingLeg(t *testing.T) {
	intent, err := intentFromSubmitTransfer(&chainv1.SubmitTransferRequest{
		IdempotencyKey: "idem-5",
		IntentRef:      "pi-5",
		OperationRef:   "op-5",
		PaymentRef:     "pay-5",
		Amount:         &moneyv1.Money{Amount: "10", Currency: "USD"},
		Metadata: map[string]string{
			metadataOutgoingLeg: "card",
		},
	}, "provider_settlement", "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got, want := intent.OutgoingLeg, discovery.RailCardPayout; got != want {
		t.Fatalf("unexpected outgoing leg: got=%q want=%q", got, want)
	}
}
