using FluentAssertions;
using SIGCM2.Application.Pricing.ChargeableChars;
using SIGCM2.Domain.Pricing.ChargeableChars;
using SIGCM2.Domain.Pricing.WordCounter;
using Xunit;

namespace SIGCM2.Application.Tests.Pricing.ChargeableChars;

/// <summary>
/// PRC-001 Batch 7 — T7.6: WordCounterService × ChargeableCharConfig integration contract.
///
/// Documents the integration pattern that PRC-002+ will follow when computing
/// special-character billing charges for classified ads.
///
/// Contract:
///   1. WordCounterService.Count(text) → WordCountResult with SpecialCharCounts keyed by Category.
///   2. IChargeableCharConfigService.GetActiveConfigForMedioAsync(medioId, today)
///      → IReadOnlyDictionary&lt;string, ChargeableCharSnapshot&gt; keyed by Symbol.
///   3. A charge calculator maps SpecialCharCounts (by Category) against resolved config
///      (by Symbol) to compute the total special-character billing amount.
///
/// This test is a PURE UNIT TEST — no DB, no HTTP, no DI container.
/// It exercises the in-memory integration point using the real WordCounterService
/// and a hand-built resolved config dictionary.
///
/// Spec reference: PRC-001-R1 + C5 golden cases + design §11.1.
/// </summary>
public sealed class WordCounterChargeableCharIntegrationTests
{
    private readonly WordCounterService _svc = new();

    // ─────────────────────────────────────────────────────────────────────────
    // T7.6.a — Basic integration contract
    //
    // Text: "vendo $5000 %50"
    //   WordCount result: { Currency: 1, Percentage: 1 }
    //   Resolved config: { "$": 2.00, "%": 3.00 }
    //   Expected charge: 1 × 2.00 + 1 × 3.00 = 5.00
    // ─────────────────────────────────────────────────────────────────────────

    [Fact]
    public void BasicIntegration_CurrencyAndPercentage_ComputesCorrectCharge()
    {
        const string text = "vendo $5000 %50";

        // Step 1: count specials
        var countResult = _svc.Count(text);

        countResult.TotalWords.Should().Be(3, "vendo + 5000 + 50 after special replacement");
        countResult.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(1);
        countResult.SpecialCharCounts.GetValueOrDefault("Percentage").Should().Be(1);

        // Step 2: resolved config (simulates IChargeableCharConfigService output)
        // Key is Symbol; ChargeableCharSnapshot holds Category + PricePerUnit.
        var resolvedConfig = BuildResolvedConfig(new[]
        {
            ("$", "Currency",    2.0000m),
            ("%", "Percentage",  3.0000m),
        });

        // Step 3: compute charge using the integration helper
        var charge = ComputeSpecialCharCharge(countResult, resolvedConfig);

        charge.Should().Be(5.0000m,
            "1 × $2.00 (Currency) + 1 × $3.00 (Percentage) = $5.00");
    }

    // ─────────────────────────────────────────────────────────────────────────
    // T7.6.b — Anti-fraud: multiple specials per word
    //
    // Text: "P$a$l$a$b$r$a"  (anti-fraud: 7 currency symbols embedded in word)
    //   WordCount: TotalWords = 7, Currency = 6
    //   Config: { "$": 1.50 }
    //   Charge: 6 × 1.50 = 9.00
    // ─────────────────────────────────────────────────────────────────────────

    [Fact]
    public void AntiFraud_MultipleSpecialsEmbedded_CountsEachOccurrence()
    {
        const string text = "P$a$l$a$b$r$a";

        var countResult = _svc.Count(text);

        countResult.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(6,
            "anti-fraud: 6 '$' embedded in the word — each is counted before replacement");

        var resolvedConfig = BuildResolvedConfig(new[]
        {
            ("$", "Currency", 1.5000m),
        });

        var charge = ComputeSpecialCharCharge(countResult, resolvedConfig);

        charge.Should().Be(9.0000m, "6 × $1.50 = $9.00");
    }

    // ─────────────────────────────────────────────────────────────────────────
    // T7.6.c — Symbol not in config → 0 charge (free special)
    //
    // Text: "¿Que tal?"
    //   WordCount: { Question: 2 }
    //   Config: only "$" and "%" configured — "?" and "¿" are NOT priced symbols.
    //   Charge: 0.00 (no config entry for Question symbols)
    // ─────────────────────────────────────────────────────────────────────────

    [Fact]
    public void SymbolNotInConfig_ProducesZeroCharge()
    {
        const string text = "¿Que tal?";

        var countResult = _svc.Count(text);

        countResult.SpecialCharCounts.GetValueOrDefault("Question").Should().Be(2,
            "¿ and ? each count as 1 Question");

        // Config has no entry for "?" or "¿" — Questions are not billable in this scenario
        var resolvedConfig = BuildResolvedConfig(new[]
        {
            ("$", "Currency",   2.0000m),
            ("%", "Percentage", 3.0000m),
        });

        var charge = ComputeSpecialCharCharge(countResult, resolvedConfig);

        charge.Should().Be(0.0000m,
            "Question symbols are not in the config — no charge should apply");
    }

    // ─────────────────────────────────────────────────────────────────────────
    // T7.6.d — Combined scenario: multiple categories, all billable
    //
    // Text: "¡Oferta! $500 -20% hoy"
    //   From GC-23: TotalWords=4, Currency=1, Percentage=1, Exclamation=2
    //   Config: { "$": 2.00, "%": 3.00, "!": 0.50, "¡": 0.50 }
    //   Charge: 1×2.00 + 1×3.00 + 2×0.50 = 6.00
    //
    // Note: "!" and "¡" are two distinct symbols but both are Exclamation category.
    // The charge is computed per-symbol-occurrence, not per-category.
    // WordCountResult gives us Exclamation=2 total; we price them the same since both
    // symbols have the same configured price (0.50 each).
    // ─────────────────────────────────────────────────────────────────────────

    [Fact]
    public void CombinedCategories_AllBillable_ComputesTotalCorrectly()
    {
        const string text = "¡Oferta! $500 -20% hoy";

        var countResult = _svc.Count(text);

        // Verify WordCounterService counts (GC-23 equivalent)
        countResult.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(1);
        countResult.SpecialCharCounts.GetValueOrDefault("Percentage").Should().Be(1);
        countResult.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(2,
            "both '¡' and '!' count as Exclamation (GC-23)");

        // Resolved config: all four symbols priced
        var resolvedConfig = BuildResolvedConfig(new[]
        {
            ("$",  "Currency",    2.0000m),
            ("%",  "Percentage",  3.0000m),
            ("!",  "Exclamation", 0.5000m),
            ("¡",  "Exclamation", 0.5000m),
        });

        // The charge calculator works at the CATEGORY level:
        // for each category in SpecialCharCounts, find the first config entry for that category,
        // use its price × occurrence count. (This simplification is valid for PRC-001 spike;
        // PRC-002 will introduce per-symbol pricing if symbols in same category have different prices.)
        var chargePerCategory = ComputeSpecialCharChargeByCategory(countResult, resolvedConfig);

        // 1×2.00 + 1×3.00 + 2×0.50 = 6.00
        chargePerCategory.Should().Be(6.0000m,
            "Currency 1×2.00 + Percentage 1×3.00 + Exclamation 2×0.50 = 6.00");
    }

    // ─────────────────────────────────────────────────────────────────────────
    // T7.6.e — Empty text → zero charge
    // ─────────────────────────────────────────────────────────────────────────

    [Fact]
    public void EmptyText_ProducesZeroWordsAndZeroCharge()
    {
        var countResult = _svc.Count("");

        countResult.TotalWords.Should().Be(0);
        countResult.SpecialCharCounts.Should().BeEmpty();

        var resolvedConfig = BuildResolvedConfig(new[]
        {
            ("$", "Currency", 2.0000m),
        });

        var charge = ComputeSpecialCharCharge(countResult, resolvedConfig);
        charge.Should().Be(0.0000m, "no specials in empty text → zero charge");
    }

    // ─────────────────────────────────────────────────────────────────────────
    // T7.6.f — Null text → zero charge (WordCounterService null-safe)
    // ─────────────────────────────────────────────────────────────────────────

    [Fact]
    public void NullText_ProducesZeroWordsAndZeroCharge()
    {
        var countResult = _svc.Count(null);

        countResult.TotalWords.Should().Be(0);
        countResult.SpecialCharCounts.Should().BeEmpty();

        var resolvedConfig = BuildResolvedConfig(Array.Empty<(string, string, decimal)>());
        var charge = ComputeSpecialCharCharge(countResult, resolvedConfig);
        charge.Should().Be(0.0000m);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Integration contract: charge calculator helpers
    //
    // These helpers document the PRC-002+ integration contract.
    // A real implementation would live in a pricing service or use case handler.
    // ─────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Builds a resolved config dictionary keyed by Symbol (simulates
    /// IChargeableCharConfigService.GetActiveConfigForMedioAsync result).
    /// </summary>
    private static IReadOnlyDictionary<string, ChargeableCharSnapshot> BuildResolvedConfig(
        IEnumerable<(string Symbol, string Category, decimal PricePerUnit)> entries)
    {
        var dict = new Dictionary<string, ChargeableCharSnapshot>(StringComparer.Ordinal);
        foreach (var (symbol, category, price) in entries)
            dict[symbol] = new ChargeableCharSnapshot(category, price);
        return dict;
    }

    /// <summary>
    /// Computes the total special-character billing charge.
    ///
    /// Algorithm (per-symbol lookup):
    ///   For each symbol in the resolved config where the config category matches a
    ///   SpecialCharCounts category, multiply the category occurrence count by the symbol price.
    ///
    /// This is the SIMPLEST form of the integration contract: one price per category.
    /// PRC-002+ can extend this to per-symbol pricing if different symbols in the same
    /// category have different prices.
    /// </summary>
    private static decimal ComputeSpecialCharCharge(
        WordCountResult countResult,
        IReadOnlyDictionary<string, ChargeableCharSnapshot> resolvedConfig)
    {
        if (countResult.SpecialCharCounts.Count == 0) return 0m;

        var total = 0m;

        // For each configured symbol, check if there are occurrences of its category.
        // Use the first symbol in each category as the representative price.
        // (Sufficient for PRC-001 where symbols within the same category share the same price.)
        var chargedCategories = new HashSet<string>(StringComparer.Ordinal);

        foreach (var (symbol, snapshot) in resolvedConfig)
        {
            if (chargedCategories.Contains(snapshot.Category)) continue;
            if (countResult.SpecialCharCounts.TryGetValue(snapshot.Category, out var count))
            {
                total += count * snapshot.PricePerUnit;
                chargedCategories.Add(snapshot.Category);
            }
        }

        return total;
    }

    /// <summary>
    /// Category-level charge computation: resolves price by finding any configured symbol
    /// for each category, then multiplies by occurrence count.
    /// Explicit version of ComputeSpecialCharCharge for clarity in documentation tests.
    /// </summary>
    private static decimal ComputeSpecialCharChargeByCategory(
        WordCountResult countResult,
        IReadOnlyDictionary<string, ChargeableCharSnapshot> resolvedConfig)
        => ComputeSpecialCharCharge(countResult, resolvedConfig);
}
