package resolver

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

	"github.com/tech/sendico/billing/fees/storage"
	"github.com/tech/sendico/billing/fees/storage/model"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

func TestResolver_GlobalFallbackWhenOrgMissing(t *testing.T) {
	t.Parallel()

	now := time.Now()
	globalPlan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-time.Hour),
		Rules: []model.FeeRule{
			{RuleID: "global_capture", Trigger: model.TriggerCapture, Priority: 5, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
		},
	}

	store := &memoryPlansStore{plans: []*model.FeePlan{globalPlan}}
	resolver := New(store, zap.NewNop())

	orgA := bson.NewObjectID()

	plan, rule, err := resolver.ResolveFeeRule(context.Background(), &orgA, model.TriggerCapture, now, nil)
	if err != nil {
		t.Fatalf("expected fallback to global, got error: %v", err)
	}

	if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() {
		t.Fatalf("expected global plan, got orgRef %s", plan.OrganizationRef.Hex())
	}

	if rule.RuleID != "global_capture" {
		t.Fatalf("unexpected rule selected: %s", rule.RuleID)
	}
}

func TestResolver_OrgOverridesGlobal(t *testing.T) {
	t.Parallel()

	now := time.Now()
	org := bson.NewObjectID()
	globalPlan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-time.Hour),
		Rules: []model.FeeRule{
			{RuleID: "global_capture", Trigger: model.TriggerCapture, Priority: 5, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
		},
	}
	orgPlan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-time.Hour),
		Rules: []model.FeeRule{
			{RuleID: "org_capture", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
		},
	}
	orgPlan.OrganizationRef = &org
	store := &memoryPlansStore{plans: []*model.FeePlan{globalPlan, orgPlan}}
	resolver := New(store, zap.NewNop())

	_, rule, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil)
	if err != nil {
		t.Fatalf("expected org plan rule, got error: %v", err)
	}

	if rule.RuleID != "org_capture" {
		t.Fatalf("expected org rule, got %s", rule.RuleID)
	}

	otherOrg := bson.NewObjectID()

	_, rule, err = resolver.ResolveFeeRule(context.Background(), &otherOrg, model.TriggerCapture, now, nil)
	if err != nil {
		t.Fatalf("expected global fallback for other org, got error: %v", err)
	}

	if rule.RuleID != "global_capture" {
		t.Fatalf("expected global rule, got %s", rule.RuleID)
	}
}

func TestResolver_SelectsHighestPriority(t *testing.T) {
	t.Parallel()

	now := time.Now()
	org := bson.NewObjectID()
	plan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-time.Hour),
		Rules: []model.FeeRule{
			{RuleID: "low", Trigger: model.TriggerCapture, Priority: 100, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
			{RuleID: "high", Trigger: model.TriggerCapture, Priority: 200, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
		},
	}
	plan.OrganizationRef = &org

	store := &memoryPlansStore{plans: []*model.FeePlan{plan}}
	resolver := New(store, zap.NewNop())

	_, rule, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil)
	if err != nil {
		t.Fatalf("expected rule resolution, got error: %v", err)
	}

	if rule.RuleID != "high" {
		t.Fatalf("expected highest priority rule, got %s", rule.RuleID)
	}

	plan.Rules = append(plan.Rules, model.FeeRule{
		RuleID:        "conflict",
		Trigger:       model.TriggerCapture,
		Priority:      200,
		Percentage:    "0.02",
		EffectiveFrom: now.Add(-time.Hour),
	})

	if _, _, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil); !errors.Is(err, ErrConflictingFeeRules) {
		t.Fatalf("expected conflicting fee rules error, got %v", err)
	}
}

func TestResolver_EffectiveDateFiltering(t *testing.T) {
	t.Parallel()

	now := time.Now()
	org := bson.NewObjectID()
	past := now.Add(-24 * time.Hour)
	future := now.Add(24 * time.Hour)

	orgPlan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: past,
		Rules: []model.FeeRule{
			{RuleID: "expired", Trigger: model.TriggerCapture, Priority: 100, Percentage: "0.05", EffectiveFrom: past, EffectiveTo: &past},
		},
	}
	orgPlan.OrganizationRef = &org

	globalPlan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: past,
		Rules: []model.FeeRule{
			{RuleID: "current", Trigger: model.TriggerCapture, Priority: 100, Percentage: "0.05", EffectiveFrom: past, EffectiveTo: &future},
		},
	}

	store := &memoryPlansStore{plans: []*model.FeePlan{orgPlan, globalPlan}}
	resolver := New(store, zap.NewNop())

	_, rule, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil)
	if err != nil {
		t.Fatalf("expected fallback to global, got error: %v", err)
	}

	if rule.RuleID != "current" {
		t.Fatalf("expected current global rule, got %s", rule.RuleID)
	}
}

func TestResolver_AppliesToFiltering(t *testing.T) {
	t.Parallel()

	now := time.Now()
	plan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-time.Hour),
		Rules: []model.FeeRule{
			{RuleID: "card", Trigger: model.TriggerCapture, Priority: 200, Percentage: "0.03", AppliesTo: map[string]string{"paymentMethod": "card"}, EffectiveFrom: now.Add(-time.Hour)},
			{RuleID: "default", Trigger: model.TriggerCapture, Priority: 100, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
		},
	}
	store := &memoryPlansStore{plans: []*model.FeePlan{plan}}
	resolver := New(store, zap.NewNop())

	_, rule, err := resolver.ResolveFeeRule(context.Background(), nil, model.TriggerCapture, now, map[string]string{"paymentMethod": "card"})
	if err != nil {
		t.Fatalf("expected card rule, got error: %v", err)
	}

	if rule.RuleID != "card" {
		t.Fatalf("expected card rule, got %s", rule.RuleID)
	}

	_, rule, err = resolver.ResolveFeeRule(context.Background(), nil, model.TriggerCapture, now, map[string]string{"paymentMethod": "bank"})
	if err != nil {
		t.Fatalf("expected default rule, got error: %v", err)
	}

	if rule.RuleID != "default" {
		t.Fatalf("expected default rule, got %s", rule.RuleID)
	}
}

func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
	t.Parallel()

	now := time.Now()
	plan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-time.Hour),
		Rules: []model.FeeRule{
			{RuleID: "network_multi", Trigger: model.TriggerCapture, Priority: 300, Percentage: "0.03", AppliesTo: map[string]string{"network": "tron, solana"}, EffectiveFrom: now.Add(-time.Hour)},
			{RuleID: "asset_any", Trigger: model.TriggerCapture, Priority: 200, Percentage: "0.02", AppliesTo: map[string]string{"asset": "*"}, EffectiveFrom: now.Add(-time.Hour)},
			{RuleID: "default", Trigger: model.TriggerCapture, Priority: 100, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
		},
	}
	store := &memoryPlansStore{plans: []*model.FeePlan{plan}}
	resolver := New(store, zap.NewNop())

	_, rule, err := resolver.ResolveFeeRule(context.Background(), nil, model.TriggerCapture, now, map[string]string{"network": "tron"})
	if err != nil {
		t.Fatalf("expected list match rule, got error: %v", err)
	}

	if rule.RuleID != "network_multi" {
		t.Fatalf("expected network list rule, got %s", rule.RuleID)
	}

	_, rule, err = resolver.ResolveFeeRule(context.Background(), nil, model.TriggerCapture, now, map[string]string{"asset": "USDT"})
	if err != nil {
		t.Fatalf("expected wildcard rule, got error: %v", err)
	}

	if rule.RuleID != "asset_any" {
		t.Fatalf("expected asset wildcard rule, got %s", rule.RuleID)
	}

	_, rule, err = resolver.ResolveFeeRule(context.Background(), nil, model.TriggerCapture, now, map[string]string{"network": "eth"})
	if err != nil {
		t.Fatalf("expected default rule, got error: %v", err)
	}

	if rule.RuleID != "default" {
		t.Fatalf("expected default rule, got %s", rule.RuleID)
	}
}

func TestResolver_MissingTriggerReturnsErr(t *testing.T) {
	t.Parallel()

	now := time.Now()
	plan := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-time.Hour),
		Rules: []model.FeeRule{
			{RuleID: "capture", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
		},
	}
	store := &memoryPlansStore{plans: []*model.FeePlan{plan}}
	resolver := New(store, zap.NewNop())

	if _, _, err := resolver.ResolveFeeRule(context.Background(), nil, model.TriggerRefund, now, nil); !errors.Is(err, ErrNoFeeRuleFound) {
		t.Fatalf("expected ErrNoFeeRuleFound, got %v", err)
	}
}

func TestResolver_MultipleActivePlansConflict(t *testing.T) {
	t.Parallel()

	now := time.Now()
	org := bson.NewObjectID()
	plan1 := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-time.Hour),
		Rules: []model.FeeRule{
			{RuleID: "r1", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
		},
	}
	plan1.OrganizationRef = &org
	plan2 := &model.FeePlan{
		Active:        true,
		EffectiveFrom: now.Add(-30 * time.Minute),
		Rules: []model.FeeRule{
			{RuleID: "r2", Trigger: model.TriggerCapture, Priority: 20, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
		},
	}
	plan2.OrganizationRef = &org

	store := &memoryPlansStore{plans: []*model.FeePlan{plan1, plan2}}
	resolver := New(store, zap.NewNop())

	if _, _, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil); !errors.Is(err, storage.ErrConflictingFeePlans) {
		t.Fatalf("expected conflicting plans error, got %v", err)
	}
}

type memoryPlansStore struct {
	plans []*model.FeePlan
}

func (m *memoryPlansStore) Create(context.Context, *model.FeePlan) error { return nil }
func (m *memoryPlansStore) Update(context.Context, *model.FeePlan) error { return nil }
func (m *memoryPlansStore) Get(context.Context, bson.ObjectID) (*model.FeePlan, error) {
	return nil, storage.ErrFeePlanNotFound
}

func (m *memoryPlansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) {
	if !orgRef.IsZero() {
		if plan, err := m.FindActiveOrgPlan(ctx, orgRef, asOf); err == nil {
			return plan, nil
		} else if !errors.Is(err, storage.ErrFeePlanNotFound) {
			return nil, err
		}
	}

	return m.FindActiveGlobalPlan(ctx, asOf)
}

func (m *memoryPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) {
	var matches []*model.FeePlan

	for _, plan := range m.plans {
		if plan == nil || plan.OrganizationRef == nil || plan.OrganizationRef.IsZero() || (*plan.OrganizationRef != orgRef) {
			continue
		}

		if !plan.Active {
			continue
		}

		if plan.EffectiveFrom.After(asOf) {
			continue
		}

		if plan.EffectiveTo != nil && !plan.EffectiveTo.After(asOf) {
			continue
		}

		matches = append(matches, plan)
	}

	if len(matches) == 0 {
		return nil, storage.ErrFeePlanNotFound
	}

	if len(matches) > 1 {
		return nil, storage.ErrConflictingFeePlans
	}

	return matches[0], nil
}

func (m *memoryPlansStore) FindActiveGlobalPlan(_ context.Context, asOf time.Time) (*model.FeePlan, error) {
	var matches []*model.FeePlan

	for _, plan := range m.plans {
		if plan == nil || ((plan.OrganizationRef != nil) && !plan.OrganizationRef.IsZero()) {
			continue
		}

		if !plan.Active {
			continue
		}

		if plan.EffectiveFrom.After(asOf) {
			continue
		}

		if plan.EffectiveTo != nil && !plan.EffectiveTo.After(asOf) {
			continue
		}

		matches = append(matches, plan)
	}

	if len(matches) == 0 {
		return nil, storage.ErrFeePlanNotFound
	}

	if len(matches) > 1 {
		return nil, storage.ErrConflictingFeePlans
	}

	return matches[0], nil
}

var _ storage.PlansStore = (*memoryPlansStore)(nil)
