"""Tests for Jinja template filters (pure-logic functions)."""
from __future__ import annotations

from decimal import Decimal

from markupsafe import Markup

from shared.browser.app.filters.highlight import highlight
from shared.browser.app.filters.combine import _deep_merge
from shared.browser.app.filters.qs_base import (
    _iterify,
    _norm,
    make_filter_set,
    build_qs,
)


# ---------------------------------------------------------------------------
# highlight
# ---------------------------------------------------------------------------

class TestHighlight:
    def test_basic_highlight(self):
        result = highlight("Hello World", "world")
        assert isinstance(result, Markup)
        assert "<mark" in result
        assert "World" in result

    def test_case_insensitive(self):
        result = highlight("Hello World", "HELLO")
        assert "<mark" in result

    def test_empty_needle(self):
        result = highlight("Hello", "")
        assert result == "Hello"

    def test_empty_text(self):
        result = highlight("", "needle")
        assert result == ""

    def test_none_text(self):
        result = highlight(None, "needle")
        assert result == ""

    def test_no_match(self):
        result = highlight("Hello World", "xyz")
        assert "<mark" not in result
        assert "Hello World" in result

    def test_escapes_html(self):
        result = highlight("<script>alert('xss')</script>", "script")
        assert "<script>" not in str(result)
        assert "&lt;script&gt;" in str(result) or "&lt;" in str(result)

    def test_custom_class(self):
        result = highlight("Hello", "Hello", cls="highlight-red")
        assert "highlight-red" in result


# ---------------------------------------------------------------------------
# truncate (tested as pure function)
# ---------------------------------------------------------------------------

class TestTruncate:
    """Test the truncate logic directly."""

    @staticmethod
    def _truncate(text, max_length=100):
        if text is None:
            return ""
        text = str(text)
        if len(text) <= max_length:
            return text
        if max_length <= 1:
            return "…"
        return text[:max_length - 1] + "…"

    def test_short_text(self):
        assert self._truncate("hello", 10) == "hello"

    def test_exact_length(self):
        assert self._truncate("hello", 5) == "hello"

    def test_truncates_long(self):
        result = self._truncate("hello world", 6)
        assert result == "hello…"
        assert len(result) == 6

    def test_none_input(self):
        assert self._truncate(None) == ""

    def test_max_length_one(self):
        assert self._truncate("hello", 1) == "…"

    def test_max_length_zero(self):
        assert self._truncate("hello", 0) == "…"


# ---------------------------------------------------------------------------
# currency (tested as pure function)
# ---------------------------------------------------------------------------

class TestCurrency:
    @staticmethod
    def _currency(value, code="GBP"):
        if value is None:
            return ""
        if isinstance(value, float):
            value = Decimal(str(value))
        symbol = "£" if code == "GBP" else code
        return f"{symbol}{value:.2f}"

    def test_gbp(self):
        assert self._currency(Decimal("19.99")) == "£19.99"

    def test_none(self):
        assert self._currency(None) == ""

    def test_float_conversion(self):
        assert self._currency(19.99) == "£19.99"

    def test_non_gbp(self):
        assert self._currency(Decimal("10.00"), "EUR") == "EUR10.00"

    def test_zero(self):
        assert self._currency(Decimal("0")) == "£0.00"

    def test_integer_decimal(self):
        assert self._currency(Decimal("5")) == "£5.00"


# ---------------------------------------------------------------------------
# combine / _deep_merge
# ---------------------------------------------------------------------------

class TestDeepMerge:
    def test_simple_merge(self):
        assert _deep_merge({"a": 1}, {"b": 2}) == {"a": 1, "b": 2}

    def test_overwrite(self):
        assert _deep_merge({"a": 1}, {"a": 2}) == {"a": 2}

    def test_nested_merge(self):
        a = {"x": {"a": 1, "b": 2}}
        b = {"x": {"b": 3, "c": 4}}
        result = _deep_merge(a, b)
        assert result == {"x": {"a": 1, "b": 3, "c": 4}}

    def test_deeply_nested(self):
        a = {"x": {"y": {"z": 1}}}
        b = {"x": {"y": {"w": 2}}}
        result = _deep_merge(a, b)
        assert result == {"x": {"y": {"z": 1, "w": 2}}}

    def test_does_not_mutate_original(self):
        a = {"a": 1}
        b = {"b": 2}
        _deep_merge(a, b)
        assert a == {"a": 1}


class TestCombineFilter:
    """Test the combine filter logic inline (it's defined inside register())."""
    from typing import Any, Mapping

    @staticmethod
    def _combine(a, b, deep=False, drop_none=False):
        from collections.abc import Mapping
        if not isinstance(a, Mapping) or not isinstance(b, Mapping):
            return a
        b2 = {k: v for k, v in b.items() if not (drop_none and v is None)}
        return _deep_merge(a, b2) if deep else {**a, **b2}

    def test_non_dict_returns_a(self):
        assert self._combine("hello", {"a": 1}) == "hello"
        assert self._combine(42, {"a": 1}) == 42

    def test_shallow_merge(self):
        result = self._combine({"a": 1}, {"b": 2})
        assert result == {"a": 1, "b": 2}

    def test_deep_merge(self):
        result = self._combine({"x": {"a": 1}}, {"x": {"b": 2}}, deep=True)
        assert result == {"x": {"a": 1, "b": 2}}

    def test_drop_none(self):
        result = self._combine({"a": 1, "b": 2}, {"b": None, "c": 3}, drop_none=True)
        assert result == {"a": 1, "b": 2, "c": 3}

    def test_keep_none_when_not_dropping(self):
        result = self._combine({"a": 1}, {"a": None})
        assert result == {"a": None}


# ---------------------------------------------------------------------------
# qs_base
# ---------------------------------------------------------------------------

class TestIterify:
    def test_none(self):
        assert _iterify(None) == []

    def test_scalar(self):
        assert _iterify("hello") == ["hello"]

    def test_list(self):
        assert _iterify([1, 2]) == [1, 2]

    def test_tuple(self):
        assert _iterify((1, 2)) == (1, 2)

    def test_set(self):
        assert _iterify({1, 2}) == {1, 2}


class TestNorm:
    def test_strips_and_lowercases(self):
        assert _norm("  Hello ") == "hello"

    def test_already_lower(self):
        assert _norm("hello") == "hello"


class TestMakeFilterSet:
    def test_add_to_empty(self):
        result = make_filter_set([], "new", None, False)
        assert result == ["new"]

    def test_add_preserves_existing(self):
        result = make_filter_set(["a", "b"], "c", None, False)
        assert result == ["a", "b", "c"]

    def test_remove(self):
        result = make_filter_set(["a", "b", "c"], None, "b", False)
        assert result == ["a", "c"]

    def test_clear_filters(self):
        result = make_filter_set(["a", "b"], None, None, True)
        assert result == []

    def test_clear_then_add(self):
        result = make_filter_set(["a", "b"], "c", None, True)
        assert result == ["c"]

    def test_case_insensitive_dedup(self):
        result = make_filter_set(["Hello"], "hello", None, False)
        assert len(result) == 1

    def test_sorted_output(self):
        result = make_filter_set([], ["c", "a", "b"], None, False)
        assert result == ["a", "b", "c"]

    def test_single_select_replaces(self):
        result = make_filter_set(["old1", "old2"], "new", None, False, single_select=True)
        assert result == ["new"]

    def test_single_select_no_add_keeps_base(self):
        result = make_filter_set(["a", "b"], None, None, False, single_select=True)
        assert result == ["a", "b"]

    def test_remove_case_insensitive(self):
        result = make_filter_set(["Hello", "World"], None, "hello", False)
        assert result == ["World"]

    def test_add_none_values_filtered(self):
        result = make_filter_set([], [None, "a", None], None, False)
        assert result == ["a"]


class TestBuildQs:
    def test_basic(self):
        result = build_qs([("key", "value")])
        assert result == "?key=value"

    def test_multiple_params(self):
        result = build_qs([("a", "1"), ("b", "2")])
        assert "a=1" in result
        assert "b=2" in result
        assert result.startswith("?")

    def test_no_leading_q(self):
        result = build_qs([("key", "value")], leading_q=False)
        assert result == "key=value"

    def test_empty_params(self):
        result = build_qs([])
        assert result == ""

    def test_empty_params_no_leading(self):
        result = build_qs([], leading_q=False)
        assert result == ""
