package quote_computation_service

import (
	"context"
	"errors"
	"github.com/tech/sendico/pkg/discovery"
	"testing"

	"github.com/tech/sendico/payments/quotation/internal/service/quotation/transfer_intent_hydrator"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	"go.mongodb.org/mongo-driver/v2/bson"
)

func TestBuildPlan_ResolvesManagedWalletNetworkFromResolver(t *testing.T) {
	resolver := &fakeManagedWalletNetworkResolver{
		networks: map[string]string{
			"wallet-usdt-source": "TRON_NILE",
		},
	}
	svc := New(nil,
		WithManagedWalletNetworkResolver(resolver),
		WithGatewayRegistry(staticGatewayRegistry{
			items: []*model.GatewayInstanceDescriptor{
				{
					ID:         "crypto-arbitrum",
					InstanceID: "crypto-arbitrum",
					Rail:       discovery.RailCrypto,
					Network:    "ARBITRUM_SEPOLIA",
					Currencies: []string{"USDT"},
					Capabilities: model.RailCapabilities{
						CanPayOut: true,
					},
					IsEnabled: true,
				},
				{
					ID:         "crypto-tron",
					InstanceID: "crypto-tron",
					Rail:       discovery.RailCrypto,
					Network:    "TRON_NILE",
					Currencies: []string{"USDT"},
					Capabilities: model.RailCapabilities{
						CanPayOut: true,
					},
					IsEnabled: true,
				},
				{
					ID:         "card-gw",
					InstanceID: "card-gw",
					Rail:       discovery.RailCardPayout,
					Currencies: []string{"USDT"},
					Capabilities: model.RailCapabilities{
						CanPayOut: true,
					},
					IsEnabled: true,
				},
			},
		}),
	)

	intent := sampleCryptoToCardQuoteIntent()
	intent.Source.ManagedWallet.Asset = nil
	orgID := bson.NewObjectID()
	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-wallet-network",
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if planModel == nil || len(planModel.Items) != 1 || planModel.Items[0] == nil {
		t.Fatalf("expected one plan item")
	}
	item := planModel.Items[0]
	if got, want := item.Steps[0].GatewayID, "crypto-tron"; got != want {
		t.Fatalf("unexpected source gateway: got=%q want=%q", got, want)
	}
	if item.Route == nil || len(item.Route.GetHops()) == 0 {
		t.Fatalf("expected route hops")
	}
	if got, want := item.Route.GetHops()[0].GetNetwork(), "tron_nile"; got != want {
		t.Fatalf("unexpected source hop network: got=%q want=%q", got, want)
	}
	if got, want := resolver.calls, 1; got != want {
		t.Fatalf("unexpected resolver calls: got=%d want=%d", got, want)
	}
}

func TestBuildPlan_ManagedWalletNetworkResolverCachesByWalletRef(t *testing.T) {
	resolver := &fakeManagedWalletNetworkResolver{
		networks: map[string]string{
			"wallet-usdt-source": "TRON_NILE",
		},
	}
	svc := New(nil,
		WithManagedWalletNetworkResolver(resolver),
		WithGatewayRegistry(staticGatewayRegistry{
			items: []*model.GatewayInstanceDescriptor{
				{
					ID:         "crypto-tron",
					InstanceID: "crypto-tron",
					Rail:       discovery.RailCrypto,
					Network:    "TRON_NILE",
					Currencies: []string{"USDT"},
					Capabilities: model.RailCapabilities{
						CanPayOut: true,
					},
					IsEnabled: true,
				},
				{
					ID:         "card-gw",
					InstanceID: "card-gw",
					Rail:       discovery.RailCardPayout,
					Currencies: []string{"USDT"},
					Capabilities: model.RailCapabilities{
						CanPayOut: true,
					},
					IsEnabled: true,
				},
			},
		}),
	)

	intentA := sampleCryptoToCardQuoteIntent()
	intentA.Ref = "intent-a"
	intentA.Source.ManagedWallet.Asset = nil
	intentB := sampleCryptoToCardQuoteIntent()
	intentB.Ref = "intent-b"
	intentB.Source.ManagedWallet.Asset = nil

	orgID := bson.NewObjectID()
	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef: orgID.Hex(),
		OrganizationID:  orgID,
		PreviewOnly:     true,
		Intents:         []*transfer_intent_hydrator.QuoteIntent{intentA, intentB},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if planModel == nil || len(planModel.Items) != 2 {
		t.Fatalf("expected two plan items")
	}
	if got, want := resolver.calls, 1; got != want {
		t.Fatalf("unexpected resolver calls: got=%d want=%d", got, want)
	}
}

func TestBuildPlan_ResolvesManagedWalletAssetTokenForSourceCurrency(t *testing.T) {
	resolver := &fakeManagedWalletNetworkResolver{
		networks: map[string]string{
			"wallet-usdt-source": "TRON_NILE",
		},
		assets: map[string]*paymenttypes.Asset{
			"wallet-usdt-source": {
				Chain:       "TRON_NILE",
				TokenSymbol: "USDT",
			},
		},
	}
	svc := New(nil,
		WithManagedWalletNetworkResolver(resolver),
		WithGatewayRegistry(staticGatewayRegistry{
			items: []*model.GatewayInstanceDescriptor{
				{
					ID:         "crypto-tron",
					InstanceID: "crypto-tron",
					Rail:       discovery.RailCrypto,
					Network:    "TRON_NILE",
					Currencies: []string{"USDT"},
					Capabilities: model.RailCapabilities{
						CanPayOut: true,
					},
					IsEnabled: true,
				},
				{
					ID:         "fx-tron",
					InstanceID: "fx-tron",
					Rail:       discovery.RailProviderSettlement,
					Network:    "TRON_NILE",
					Currencies: []string{"USDT", "RUB"},
					Operations: []model.RailOperation{discovery.RailOperationFXConvert},
					IsEnabled:  true,
				},
				{
					ID:         "card-gw",
					InstanceID: "card-gw",
					Rail:       discovery.RailCardPayout,
					Currencies: []string{"RUB"},
					Capabilities: model.RailCapabilities{
						CanPayOut: true,
					},
					IsEnabled: true,
				},
			},
		}),
	)

	intent := sampleCryptoToCardQuoteIntent()
	intent.Source.ManagedWallet.Asset = nil
	intent.Amount = &paymenttypes.Money{
		Amount:   "5000",
		Currency: "RUB",
	}
	intent.SettlementMode = transfer_intent_hydrator.QuoteSettlementModeFixReceived
	intent.SettlementCurrency = "RUB"
	intent.RequiresFX = false
	orgID := bson.NewObjectID()
	planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-wallet-asset",
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if planModel == nil || len(planModel.Items) != 1 || planModel.Items[0] == nil {
		t.Fatalf("expected one plan item")
	}
	item := planModel.Items[0]
	if got, want := item.Steps[0].GatewayID, "crypto-tron"; got != want {
		t.Fatalf("unexpected source gateway: got=%q want=%q", got, want)
	}
	if item.Steps[0] == nil || item.Steps[0].Amount == nil {
		t.Fatalf("expected source step amount")
	}
	if got, want := item.Steps[0].Amount.GetCurrency(), "USDT"; got != want {
		t.Fatalf("unexpected source step currency: got=%q want=%q", got, want)
	}
	if got, want := resolver.assetCalls, 1; got != want {
		t.Fatalf("unexpected asset resolver calls: got=%d want=%d", got, want)
	}
}

func TestBuildPlan_FailsWhenManagedWalletNetworkResolutionFails(t *testing.T) {
	resolver := &fakeManagedWalletNetworkResolver{
		err: merrors.NoData("wallet not found"),
	}
	svc := New(nil, WithManagedWalletNetworkResolver(resolver))

	intent := sampleCryptoToCardQuoteIntent()
	intent.Source.ManagedWallet.Asset = nil
	orgID := bson.NewObjectID()
	_, err := svc.BuildPlan(context.Background(), ComputeInput{
		OrganizationRef:    orgID.Hex(),
		OrganizationID:     orgID,
		BaseIdempotencyKey: "idem-wallet-network-fail",
		Intents:            []*transfer_intent_hydrator.QuoteIntent{intent},
	})
	if !errors.Is(err, merrors.ErrNoData) {
		t.Fatalf("expected no_data error, got %v", err)
	}
}

type fakeManagedWalletNetworkResolver struct {
	networks   map[string]string
	assets     map[string]*paymenttypes.Asset
	err        error
	assetErr   error
	calls      int
	assetCalls int
}

func (f *fakeManagedWalletNetworkResolver) ResolveManagedWalletNetwork(_ context.Context, managedWalletRef string) (string, error) {
	f.calls++
	if f.err != nil {
		return "", f.err
	}
	if f.networks == nil {
		return "", nil
	}
	return f.networks[managedWalletRef], nil
}

func (f *fakeManagedWalletNetworkResolver) ResolveManagedWalletAsset(_ context.Context, managedWalletRef string) (*paymenttypes.Asset, error) {
	f.assetCalls++
	if f.assetErr != nil {
		return nil, f.assetErr
	}
	if f.assets == nil {
		return nil, nil //nolint:nilnil // test double: nil asset map means no resolved asset
	}
	src := f.assets[managedWalletRef]
	if src == nil {
		return nil, nil //nolint:nilnil // test double: missing key means no resolved asset
	}
	return &paymenttypes.Asset{
		Chain:           src.GetChain(),
		TokenSymbol:     src.GetTokenSymbol(),
		ContractAddress: src.GetContractAddress(),
	}, nil
}
