package orchestrator

import (
	"context"
	"testing"
	"time"

	oracleclient "github.com/tech/sendico/fx/oracle/client"
	ledgerclient "github.com/tech/sendico/ledger/client"
	"github.com/tech/sendico/payments/orchestrator/storage"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	clockpkg "github.com/tech/sendico/pkg/clock"
	mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
	"github.com/tech/sendico/pkg/model/account_role"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"google.golang.org/protobuf/types/known/structpb"
)

func TestValidateMetaAndOrgRef(t *testing.T) {
	org := bson.NewObjectID()
	meta := &orchestratorv1.RequestMeta{OrganizationRef: org.Hex()}
	ref, id, err := validateMetaAndOrgRef(meta)
	if err != nil {
		t.Fatalf("expected nil error: %v", err)
	}
	if ref != org.Hex() || id != org {
		t.Fatalf("unexpected org parsing: %s %s", ref, id.Hex())
	}
	if _, _, err := validateMetaAndOrgRef(nil); err == nil {
		t.Fatalf("expected error on nil meta")
	}
	if _, _, err := validateMetaAndOrgRef(&orchestratorv1.RequestMeta{OrganizationRef: ""}); err == nil {
		t.Fatalf("expected error on empty orgRef")
	}
	if _, _, err := validateMetaAndOrgRef(&orchestratorv1.RequestMeta{OrganizationRef: "bad"}); err == nil {
		t.Fatalf("expected error on invalid orgRef")
	}
}

func TestRequireIdempotencyKey(t *testing.T) {
	if _, err := requireIdempotencyKey("   "); err == nil {
		t.Fatalf("expected error for empty key")
	}
	val, err := requireIdempotencyKey(" key ")
	if err != nil || val != "key" {
		t.Fatalf("unexpected result %s err %v", val, err)
	}
}

func TestNewPayment(t *testing.T) {
	org := bson.NewObjectID()
	intent := &orchestratorv1.PaymentIntent{
		Ref:                "ref-1",
		Amount:             &moneyv1.Money{Currency: "USD", Amount: "10"},
		SettlementMode:     orchestratorv1.SettlementMode_SETTLEMENT_FIX_RECEIVED,
		SettlementCurrency: "USD",
	}
	quote := &orchestratorv1.PaymentQuote{QuoteRef: "q1"}
	p := newPayment(org, intent, "idem", map[string]string{"k": "v"}, quote)
	if p.PaymentRef == "" || p.IdempotencyKey != "idem" || p.State != model.PaymentStateAccepted {
		t.Fatalf("unexpected payment fields: %+v", p)
	}
	if p.Intent.Amount == nil || p.Intent.Amount.GetAmount() != "10" {
		t.Fatalf("intent not copied")
	}
	if p.Intent.SettlementMode != model.SettlementModeFixReceived {
		t.Fatalf("settlement mode not preserved")
	}
	if p.LastQuote == nil || p.LastQuote.QuoteRef != "q1" {
		t.Fatalf("quote not copied")
	}
}

func TestResolvePaymentQuote_NotFound(t *testing.T) {
	org := bson.NewObjectID()
	svc := &Service{
		storage: stubRepo{quotes: &helperQuotesStore{}},
		clock:   clockpkg.NewSystem(),
	}
	_, _, err := svc.resolvePaymentQuote(context.Background(), quoteResolutionInput{
		OrgRef: org.Hex(),
		OrgID:  org,
		Meta:   &orchestratorv1.RequestMeta{OrganizationRef: org.Hex()},
		Intent: &orchestratorv1.PaymentIntent{
			Ref:    "ref-1",
			Amount: &moneyv1.Money{Currency: "USD", Amount: "1"}, SettlementCurrency: "USD"},
		QuoteRef: "missing",
	})
	if qerr, ok := err.(quoteResolutionError); !ok || qerr.code != "quote_not_found" {
		t.Fatalf("expected quote_not_found, got %v", err)
	}
}

func TestResolvePaymentQuote_Expired(t *testing.T) {
	org := bson.NewObjectID()
	intent := &orchestratorv1.PaymentIntent{
		Ref:    "ref-1",
		Amount: &moneyv1.Money{Currency: "USD", Amount: "1"}, SettlementCurrency: "USD"}
	record := &model.PaymentQuoteRecord{
		QuoteRef:  "q1",
		Intent:    intentFromProto(intent),
		Quote:     &model.PaymentQuoteSnapshot{},
		ExpiresAt: time.Now().Add(-time.Minute),
	}
	svc := &Service{
		storage: stubRepo{quotes: &helperQuotesStore{records: map[string]*model.PaymentQuoteRecord{"q1": record}}},
		clock:   clockpkg.NewSystem(),
	}
	_, _, err := svc.resolvePaymentQuote(context.Background(), quoteResolutionInput{
		OrgRef:   org.Hex(),
		OrgID:    org,
		Meta:     &orchestratorv1.RequestMeta{OrganizationRef: org.Hex()},
		Intent:   intent,
		QuoteRef: "q1",
	})
	if qerr, ok := err.(quoteResolutionError); !ok || qerr.code != "quote_expired" {
		t.Fatalf("expected quote_expired, got %v", err)
	}
}

func TestResolvePaymentQuote_QuoteRefUsesStoredIntent(t *testing.T) {
	org := bson.NewObjectID()
	intent := &orchestratorv1.PaymentIntent{
		Ref:    "ref-1",
		Amount: &moneyv1.Money{Currency: "USD", Amount: "1"}, SettlementCurrency: "USD"}
	record := &model.PaymentQuoteRecord{
		QuoteRef: "q1",
		Intent:   intentFromProto(intent),
		Quote:    &model.PaymentQuoteSnapshot{},
	}
	svc := &Service{
		storage: stubRepo{quotes: &helperQuotesStore{records: map[string]*model.PaymentQuoteRecord{"q1": record}}},
		clock:   clockpkg.NewSystem(),
	}
	quote, resolvedIntent, err := svc.resolvePaymentQuote(context.Background(), quoteResolutionInput{
		OrgRef:   org.Hex(),
		OrgID:    org,
		Meta:     &orchestratorv1.RequestMeta{OrganizationRef: org.Hex()},
		QuoteRef: "q1",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if quote == nil || quote.GetQuoteRef() != "q1" {
		t.Fatalf("expected quote_ref q1, got %#v", quote)
	}
	if resolvedIntent == nil || resolvedIntent.GetAmount().GetAmount() != "1" {
		t.Fatalf("expected resolved intent with amount, got %#v", resolvedIntent)
	}
}

func TestResolvePaymentQuote_QuoteRefSkipsQuoteRecompute(t *testing.T) {
	org := bson.NewObjectID()
	intent := &orchestratorv1.PaymentIntent{
		Ref:                "ref-1",
		Amount:             &moneyv1.Money{Currency: "USD", Amount: "1"},
		SettlementCurrency: "USD",
	}
	record := &model.PaymentQuoteRecord{
		QuoteRef: "q1",
		Intent:   intentFromProto(intent),
		Quote:    &model.PaymentQuoteSnapshot{},
	}

	feeFake := &feeEngineFake{}
	oracleCalls := 0
	svc := &Service{
		storage: stubRepo{quotes: &helperQuotesStore{records: map[string]*model.PaymentQuoteRecord{"q1": record}}},
		clock:   clockpkg.NewSystem(),
		deps: serviceDependencies{
			fees: feesDependency{client: feeFake},
			oracle: oracleDependency{client: &oracleclient.Fake{
				GetQuoteFn: func(ctx context.Context, params oracleclient.GetQuoteParams) (*oracleclient.Quote, error) {
					oracleCalls++
					return &oracleclient.Quote{QuoteRef: "q1", ExpiresAt: time.Now()}, nil
				},
			}},
		},
	}

	_, _, err := svc.resolvePaymentQuote(context.Background(), quoteResolutionInput{
		OrgRef:   org.Hex(),
		OrgID:    org,
		Meta:     &orchestratorv1.RequestMeta{OrganizationRef: org.Hex()},
		QuoteRef: "q1",
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if feeFake.precomputeCalls != 0 {
		t.Fatalf("expected no fee recompute, got %d", feeFake.precomputeCalls)
	}
	if oracleCalls != 0 {
		t.Fatalf("expected no fx recompute, got %d", oracleCalls)
	}
}

func TestInitiatePaymentIdempotency(t *testing.T) {
	logger := mloggerfactory.NewLogger(false)
	org := bson.NewObjectID()
	store := newHelperPaymentStore()
	ledgerFake := &ledgerclient.Fake{
		ListConnectorAccountsFn: func(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
			operatingDetails, _ := structpb.NewStruct(map[string]interface{}{"role": "ACCOUNT_ROLE_OPERATING"})
			transitDetails, _ := structpb.NewStruct(map[string]interface{}{"role": "ACCOUNT_ROLE_TRANSIT"})
			return &connectorv1.ListAccountsResponse{
				Accounts: []*connectorv1.Account{
					{Ref: &connectorv1.AccountRef{ConnectorId: "ledger", AccountId: "ledger:operating"}, Kind: connectorv1.AccountKind_LEDGER_ACCOUNT, Asset: "USD", ProviderDetails: operatingDetails},
					{Ref: &connectorv1.AccountRef{ConnectorId: "ledger", AccountId: "ledger:transit"}, Kind: connectorv1.AccountKind_LEDGER_ACCOUNT, Asset: "USD", ProviderDetails: transitDetails},
				},
			}, nil
		},
		TransferInternalFn: func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
			return &ledgerv1.PostResponse{JournalEntryRef: "move-1"}, nil
		},
	}
	routes := &stubRoutesStore{
		routes: []*model.PaymentRoute{
			{FromRail: model.RailLedger, ToRail: model.RailLedger, IsEnabled: true},
		},
	}
	plans := &stubPlanTemplatesStore{
		templates: []*model.PaymentPlanTemplate{
			{
				FromRail:  model.RailLedger,
				ToRail:    model.RailLedger,
				IsEnabled: true,
				Steps: []model.OrchestrationStep{
					{StepID: "ledger_move", Rail: model.RailLedger, Operation: "ledger.move", FromRole: rolePtr(account_role.AccountRoleOperating), ToRole: rolePtr(account_role.AccountRoleTransit)},
				},
			},
		},
	}
	svc := NewService(logger, stubRepo{
		payments: store,
		routes:   routes,
		plans:    plans,
	}, WithClock(clockpkg.NewSystem()), WithLedgerClient(ledgerFake))
	svc.ensureHandlers()

	intent := &orchestratorv1.PaymentIntent{
		Ref: "ref-1",
		Source: &orchestratorv1.PaymentEndpoint{
			Endpoint: &orchestratorv1.PaymentEndpoint_Ledger{Ledger: &orchestratorv1.LedgerEndpoint{LedgerAccountRef: "ledger:source"}},
		},
		Destination: &orchestratorv1.PaymentEndpoint{
			Endpoint: &orchestratorv1.PaymentEndpoint_Ledger{Ledger: &orchestratorv1.LedgerEndpoint{LedgerAccountRef: "ledger:dest"}},
		},
		Amount:             &moneyv1.Money{Currency: "USD", Amount: "1"},
		SettlementCurrency: "USD",
	}
	req := &orchestratorv1.InitiatePaymentRequest{
		Meta:           &orchestratorv1.RequestMeta{OrganizationRef: org.Hex()},
		Intent:         intent,
		IdempotencyKey: "k1",
	}
	resp, err := svc.h.commands.InitiatePayment().Execute(context.Background(), req)(context.Background())
	if err != nil {
		t.Fatalf("first call failed: %v", err)
	}
	resp2, err := svc.h.commands.InitiatePayment().Execute(context.Background(), req)(context.Background())
	if err != nil {
		t.Fatalf("second call failed: %v", err)
	}
	if resp == nil || resp2 == nil || resp.Payment.GetPaymentRef() != resp2.Payment.GetPaymentRef() {
		t.Fatalf("idempotent call returned different payments")
	}
}

func TestInitiatePaymentByQuoteRef(t *testing.T) {
	logger := mloggerfactory.NewLogger(false)
	org := bson.NewObjectID()
	store := newHelperPaymentStore()
	intent := &orchestratorv1.PaymentIntent{
		Ref: "ref-1",
		Source: &orchestratorv1.PaymentEndpoint{
			Endpoint: &orchestratorv1.PaymentEndpoint_Ledger{Ledger: &orchestratorv1.LedgerEndpoint{LedgerAccountRef: "ledger:source"}},
		},
		Destination: &orchestratorv1.PaymentEndpoint{
			Endpoint: &orchestratorv1.PaymentEndpoint_Ledger{Ledger: &orchestratorv1.LedgerEndpoint{LedgerAccountRef: "ledger:dest"}},
		},
		Amount:             &moneyv1.Money{Currency: "USD", Amount: "1"},
		SettlementCurrency: "USD",
	}
	record := &model.PaymentQuoteRecord{
		QuoteRef: "q1",
		Intent:   intentFromProto(intent),
		Quote:    &model.PaymentQuoteSnapshot{},
	}
	ledgerFake := &ledgerclient.Fake{
		ListConnectorAccountsFn: func(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
			operatingDetails, _ := structpb.NewStruct(map[string]interface{}{"role": "ACCOUNT_ROLE_OPERATING"})
			transitDetails, _ := structpb.NewStruct(map[string]interface{}{"role": "ACCOUNT_ROLE_TRANSIT"})
			return &connectorv1.ListAccountsResponse{
				Accounts: []*connectorv1.Account{
					{Ref: &connectorv1.AccountRef{ConnectorId: "ledger", AccountId: "ledger:operating"}, Kind: connectorv1.AccountKind_LEDGER_ACCOUNT, Asset: "USD", ProviderDetails: operatingDetails},
					{Ref: &connectorv1.AccountRef{ConnectorId: "ledger", AccountId: "ledger:transit"}, Kind: connectorv1.AccountKind_LEDGER_ACCOUNT, Asset: "USD", ProviderDetails: transitDetails},
				},
			}, nil
		},
		TransferInternalFn: func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
			return &ledgerv1.PostResponse{JournalEntryRef: "move-1"}, nil
		},
	}
	routes := &stubRoutesStore{
		routes: []*model.PaymentRoute{
			{FromRail: model.RailLedger, ToRail: model.RailLedger, IsEnabled: true},
		},
	}
	plans := &stubPlanTemplatesStore{
		templates: []*model.PaymentPlanTemplate{
			{
				FromRail:  model.RailLedger,
				ToRail:    model.RailLedger,
				IsEnabled: true,
				Steps: []model.OrchestrationStep{
					{StepID: "ledger_move", Rail: model.RailLedger, Operation: "ledger.move", FromRole: rolePtr(account_role.AccountRoleOperating), ToRole: rolePtr(account_role.AccountRoleTransit)},
				},
			},
		},
	}
	svc := NewService(logger, stubRepo{
		payments: store,
		quotes:   &helperQuotesStore{records: map[string]*model.PaymentQuoteRecord{"q1": record}},
		routes:   routes,
		plans:    plans,
	}, WithClock(clockpkg.NewSystem()), WithLedgerClient(ledgerFake))
	svc.ensureHandlers()

	req := &orchestratorv1.InitiatePaymentRequest{
		Meta:           &orchestratorv1.RequestMeta{OrganizationRef: org.Hex()},
		QuoteRef:       "q1",
		IdempotencyKey: "k1",
	}
	resp, err := svc.h.commands.InitiatePayment().Execute(context.Background(), req)(context.Background())
	if err != nil {
		t.Fatalf("initiate by quote_ref failed: %v", err)
	}
	if resp == nil || resp.GetPayment() == nil {
		t.Fatalf("expected payment response")
	}
	if resp.GetPayment().GetIntent().GetAmount().GetAmount() != "1" {
		t.Fatalf("expected intent amount to be resolved from quote")
	}
	if resp.GetPayment().GetLastQuote().GetQuoteRef() != "q1" {
		t.Fatalf("expected last quote_ref to be set from stored quote")
	}
}

// --- test doubles ---

type stubRepo struct {
	payments storage.PaymentsStore
	quotes   storage.QuotesStore
	routes   storage.RoutesStore
	plans    storage.PlanTemplatesStore
	pingErr  error
}

func (s stubRepo) Ping(context.Context) error      { return s.pingErr }
func (s stubRepo) Payments() storage.PaymentsStore { return s.payments }
func (s stubRepo) Quotes() storage.QuotesStore     { return s.quotes }
func (s stubRepo) Routes() storage.RoutesStore     { return s.routes }
func (s stubRepo) PlanTemplates() storage.PlanTemplatesStore {
	if s.plans != nil {
		return s.plans
	}
	return &stubPlanTemplatesStore{}
}

type helperPaymentStore struct {
	byRef   map[string]*model.Payment
	byIdem  map[string]*model.Payment
	byChain map[string]*model.Payment
}

func newHelperPaymentStore() *helperPaymentStore {
	return &helperPaymentStore{
		byRef:   make(map[string]*model.Payment),
		byIdem:  make(map[string]*model.Payment),
		byChain: make(map[string]*model.Payment),
	}
}

func (s *helperPaymentStore) Create(_ context.Context, p *model.Payment) error {
	if _, ok := s.byRef[p.PaymentRef]; ok {
		return storage.ErrDuplicatePayment
	}
	s.byRef[p.PaymentRef] = p
	if p.IdempotencyKey != "" {
		s.byIdem[p.IdempotencyKey] = p
	}
	if p.Execution != nil && p.Execution.ChainTransferRef != "" {
		s.byChain[p.Execution.ChainTransferRef] = p
	}
	return nil
}

func (s *helperPaymentStore) Update(_ context.Context, p *model.Payment) error {
	if p == nil {
		return storage.ErrPaymentNotFound
	}
	if _, ok := s.byRef[p.PaymentRef]; !ok {
		return storage.ErrPaymentNotFound
	}
	s.byRef[p.PaymentRef] = p
	if p.IdempotencyKey != "" {
		s.byIdem[p.IdempotencyKey] = p
	}
	return nil
}

func (s *helperPaymentStore) GetByPaymentRef(_ context.Context, ref string) (*model.Payment, error) {
	if p, ok := s.byRef[ref]; ok {
		return p, nil
	}
	return nil, storage.ErrPaymentNotFound
}

func (s *helperPaymentStore) GetByIdempotencyKey(_ context.Context, _ bson.ObjectID, key string) (*model.Payment, error) {
	if p, ok := s.byIdem[key]; ok {
		return p, nil
	}
	return nil, storage.ErrPaymentNotFound
}

func (s *helperPaymentStore) GetByChainTransferRef(_ context.Context, ref string) (*model.Payment, error) {
	if p, ok := s.byChain[ref]; ok {
		return p, nil
	}
	return nil, storage.ErrPaymentNotFound
}

func (s *helperPaymentStore) List(_ context.Context, _ *model.PaymentFilter) (*model.PaymentList, error) {
	return &model.PaymentList{}, nil
}

type helperQuotesStore struct {
	records map[string]*model.PaymentQuoteRecord
}

func (s *helperQuotesStore) Create(_ context.Context, _ *model.PaymentQuoteRecord) error { return nil }

func (s *helperQuotesStore) GetByRef(_ context.Context, _ bson.ObjectID, ref string) (*model.PaymentQuoteRecord, error) {
	if s.records == nil {
		return nil, storage.ErrQuoteNotFound
	}
	if rec, ok := s.records[ref]; ok {
		return rec, nil
	}
	return nil, storage.ErrQuoteNotFound
}

func (s *helperQuotesStore) GetByIdempotencyKey(_ context.Context, orgRef bson.ObjectID, ref string) (*model.PaymentQuoteRecord, error) {
	if s.records == nil {
		return nil, storage.ErrQuoteNotFound
	}
	for _, rec := range s.records {
		if rec.OrganizationRef != orgRef {
			continue
		}
		if rec.IdempotencyKey == ref {
			return rec, nil
		}
	}
	return nil, storage.ErrQuoteNotFound
}

func rolePtr(role account_role.AccountRole) *account_role.AccountRole {
	return &role
}
