"""Unit tests: variable engine, invoice numbers, random financial generators."""
import string

import pytest

from app.services.variable_engine import (
    GeneratedValues,
    VariableEngine,
    analyze_template_variables,
)


class TestInvoiceGeneration:
    def test_default_format_shape(self):
        ve = VariableEngine()
        inv = ve.generate_invoice_number()
        assert inv.startswith("INV-")
        # {R4}-{R5}-{R6}-{seq4}: total = 4+1+5+1+6+1+4 = 22 chars
        assert len(inv) == len("INV-") + 22
        parts = inv[len("INV-"):].split("-")
        assert len(parts) == 4
        for p in parts:
            assert p, "empty block"
        for ch in parts[0]:
            assert ch in string.ascii_uppercase + string.digits

    def test_crypto_strong_distinct_values(self):
        ve = VariableEngine()
        seen = set()
        for _ in range(200):
            seen.add(ve.generate_invoice_number(used=seen))
        assert len(seen) == 200

    def test_collision_checking_avoids_used(self):
        # Deterministic: a scripted RNG enumerates the 100-value {d2} space in
        # order, so every generate returns a fresh unused value. With rejection
        # sampling against SystemRandom this test was flaky near exhaustion
        # (only 1 free value left -> ~60% chance of missing it in 50 attempts).
        class _ScriptedRng:
            def __init__(self, seq):
                self.seq, self.i = seq, 0

            def choice(self, chars):
                v = self.seq[self.i % len(self.seq)]
                self.i += 1
                return v

        digit_seq = []
        for v in range(100):
            digit_seq += [str(v // 10 % 10), str(v % 10)]  # renders "00", "01", ... "99"
        ve = VariableEngine({"invoice_format": "{d2}"}, rng=_ScriptedRng(digit_seq))
        used = set()
        for _ in range(100):
            used.add(ve.generate_invoice_number(used=used))
        assert len(used) == 100
        # space exhausted: 50 rejected attempts then a hard failure (no fake success)
        with pytest.raises(ValueError):
            ve.generate_invoice_number(used=used)

    def test_custom_prefix_and_format(self):
        ve = VariableEngine({"invoice_prefix": "ACME-", "invoice_format": "{prefix}{r6}"})
        inv = ve.generate_invoice_number()
        assert inv.startswith("ACME-")
        body = inv[len("ACME-"):]
        assert len(body) == 6
        for ch in body:
            assert ch in string.ascii_lowercase + string.digits

    def test_date_token(self):
        ve = VariableEngine({"invoice_format": "{date}"})
        inv = ve.generate_invoice_number()
        assert len(inv) == 8 and inv.isdigit()

    def test_hash_token(self):
        ve = VariableEngine({"invoice_format": "{hash4}"})
        inv = ve.generate_invoice_number()
        # token_hex(4) -> 8 hex chars
        assert len(inv) == 8
        int(inv, 16)  # must be hex

    def test_lowercase_tokens_and_unknown_token_passthrough(self):
        ve = VariableEngine({"invoice_format": "{d3}-{xxx}"})
        inv = ve.generate_invoice_number()
        assert inv[0:3].isdigit()
        assert "{xxx}" in inv  # unknown token left as literal


class TestRandomFinancial:
    def test_amount_within_range(self):
        ve = VariableEngine({"amount_min": 100, "amount_max": 200})
        for _ in range(50):
            a = ve.generate_amount()
            assert 100 <= a <= 200
            assert a == round(a, 2)

    def test_amount_default_range(self):
        ve = VariableEngine()
        for _ in range(20):
            a = ve.generate_amount()
            assert 300 <= a <= 1500

    def test_amount_swapped_bounds(self):
        ve = VariableEngine({"amount_min": 500, "amount_max": 100})
        a = ve.generate_amount()
        assert 100 <= a <= 500

    def test_shipping_within_range(self):
        ve = VariableEngine({"shipping_min": 5, "shipping_max": 50})
        for _ in range(50):
            s = ve.generate_shipping()
            assert 5 <= s <= 50

    def test_random_name_format(self):
        ve = VariableEngine()
        n = ve.generate_random_name()
        first, last = n.split(" ")
        assert first and last and first.istitle() and last.istitle()


class TestSubstitution:
    def _gen(self, **kw):
        return GeneratedValues(
            invoice_number="INV-AB123-CD456",
            amount=1240.0,
            shipping=18.5,
            random_name="Jordan Lee",
            invoice_date="2025-01-15",
            due_date="2025-01-30",
            phone="+1 555 0100",
            **kw,
        )

    def test_all_known_variables_substituted(self):
        ve = VariableEngine()
        text = (
            "Dear %FirstName% %LastName%, "
            "invoice %RandomInvoiceNumber% for %RandomTotal% "
            "(shipping %RandomShippingCharge%) due %DueDate%, "
            "dated %InvoiceDate%. Contact %CompanyPhone%. "
            "%CompanyName% — %CustomerEmail% — %Email% — %RandomName%. "
            "Unsubscribe: %UnsubscribeLink%"
        )
        r = ve.substitute(text, {"first_name": "Alex", "last_name": "Rivera",
                                 "company_name": "Acme Corp", "email": "alex@example.com",
                                 "customer_email": "alex@example.com"},
                          self._gen(), extra={"unsubscribe_link": "https://x.test/u?token=1"})
        assert r.unknown == []
        assert r.missing == []
        assert "Alex" in r.text and "Rivera" in r.text
        assert "INV-AB123-CD456" in r.text
        assert "$1,240.00" in r.text
        assert "$18.50" in r.text
        assert "2025-01-30" in r.text and "2025-01-15" in r.text
        assert "+1 555 0100" in r.text
        assert "Acme Corp" in r.text
        assert "Jordan Lee" in r.text
        assert "https://x.test/u?token=1" in r.text
        assert "%" not in r.text

    def test_missing_value_marked_not_faked(self):
        ve = VariableEngine()
        r = ve.substitute("Hi %FirstName%", {"first_name": ""}, self._gen())
        assert "[FirstName]" in r.text
        assert "FirstName" in r.missing
        assert "[FirstName]" not in ve.substitute("Hi %FirstName%", {"first_name": "Bo"}, self._gen()).text

    def test_unknown_variable_flagged(self):
        ve = VariableEngine()
        r = ve.substitute("Hi %NoSuchVar%", {}, self._gen())
        assert "[UNKNOWN:NoSuchVar]" in r.text
        assert r.unknown == ["NoSuchVar"]

    def test_custom_csv_columns(self):
        ve = VariableEngine()
        r = ve.substitute("Order %OrderRef% for %FirstName%",
                          {"first_name": "Alex"},
                          self._gen(), custom_fields={"OrderRef": "ORD-77"})
        assert "ORD-77" in r.text
        assert "OrderRef" not in r.unknown

    def test_orm_style_recipient_dict_via_snake_to_camel(self):
        ve = VariableEngine()
        r = ve.substitute("%FirstName% %LastName% of %CompanyName%",
                          {"first_name": "Ann", "last_name": "Kim", "company_name": "Zeta"},
                          self._gen())
        assert r.text == "Ann Kim of Zeta"

    def test_used_list_populated(self):
        ve = VariableEngine()
        r = ve.substitute("%FirstName% and %RandomTotal%", {"first_name": "Alex"}, self._gen())
        assert "FirstName" in r.used and "RandomTotal" in r.used


class TestAnalysis:
    def test_analyze_reports_unknown(self):
        a = analyze_template_variables("Dear %FirstName%, ref %InvoiceNumber%")
        assert a["variables"] == ["FirstName", "InvoiceNumber"]
        assert a["unknown"] == ["InvoiceNumber"]

    def test_analyze_with_custom_fields(self):
        a = analyze_template_variables("%OrderRef% %FirstName%", custom_fields=["OrderRef"])
        assert a["unknown"] == []

    def test_analyze_no_vars(self):
        a = analyze_template_variables("plain text no vars")
        assert a == {"variables": [], "unknown": []}
