import collections
import collections.abc
import datetime
import re
import typing

import pytest

from zodchy import codex
from zodchy_notations.query.math import Parser


def test_parser_handles_basic_query_components() -> None:
    parser = Parser()
    query = "order_by=-created_at,name&limit=10&offset=5&status=active&is_active=true&category=!archived"
    types_map = {
        "created_at": datetime.datetime,
        "name": str,
        "status": str,
        "is_active": bool,
        "category": str,
    }

    clauses = list(parser(query, types_map))

    created_name, created_clause = clauses[0]
    assert created_name == "created_at"
    assert isinstance(created_clause, codex.operator.DESC)
    assert created_clause.value == 0

    name_name, name_clause = clauses[1]
    assert name_name == "name"
    assert isinstance(name_clause, codex.operator.ASC)
    assert name_clause.value == 1

    limit_name, limit_clause = clauses[2]
    assert limit_name == "limit"
    assert isinstance(limit_clause, codex.operator.Limit)
    assert limit_clause.value == 10

    offset_name, offset_clause = clauses[3]
    assert offset_name == "offset"
    assert isinstance(offset_clause, codex.operator.Offset)
    assert offset_clause.value == 5

    status_name, status_clause = clauses[4]
    assert status_name == "status"
    assert isinstance(status_clause, codex.operator.EQ)
    assert status_clause.value == "active"

    active_name, active_clause = clauses[5]
    assert active_name == "is_active"
    assert isinstance(active_clause, codex.operator.EQ)
    assert active_clause.value is True

    category_name, category_clause = clauses[6]
    assert category_name == "category"
    assert isinstance(category_clause, codex.operator.NOT)
    assert isinstance(category_clause.value, codex.operator.EQ)
    assert category_clause.value.value == "archived"


def test_parser_interprets_intervals_sets_and_nulls() -> None:
    parser = Parser()
    types_map = {
        "created_at": datetime.datetime,
        "score": float,
        "tags": str,
        "excluded_tags": str,
        "deleted_at": datetime.datetime,
        "status": str,
    }
    query: collections.OrderedDict[str, str] = collections.OrderedDict(
        [
            ("created_at", "[2024-01-01T00:00:00,2024-02-01T00:00:00)"),
            ("score", "(10,20]"),
            ("tags", "{alpha,beta,gamma}"),
            ("excluded_tags", "!{delta,epsilon}"),
            ("deleted_at", "null"),
            ("status", "!null"),
        ]
    )

    clauses = list(parser(query, types_map))

    _, date_range = clauses[0]
    assert isinstance(date_range, codex.operator.RANGE)
    left, right = date_range.value
    assert isinstance(left, codex.operator.GE)
    assert isinstance(right, codex.operator.LT)
    assert left.value == datetime.datetime(2024, 1, 1, 0, 0)
    assert right.value == datetime.datetime(2024, 2, 1, 0, 0)

    _, score_range = clauses[1]
    assert isinstance(score_range, codex.operator.RANGE)
    score_left, score_right = score_range.value
    assert isinstance(score_left, codex.operator.GT)
    assert isinstance(score_right, codex.operator.LE)
    assert score_left.value == 10.0
    assert score_right.value == 20.0

    _, tag_clause = clauses[2]
    assert isinstance(tag_clause, codex.operator.SET)
    assert tag_clause.value == {"alpha", "beta", "gamma"}

    _, excluded_clause = clauses[3]
    assert isinstance(excluded_clause, codex.operator.NOT)
    assert isinstance(excluded_clause.value, codex.operator.SET)
    assert excluded_clause.value.value == {"delta", "epsilon"}

    _, deleted_clause = clauses[4]
    assert isinstance(deleted_clause, codex.operator.IS)
    assert deleted_clause.value is None

    _, status_clause = clauses[5]
    assert isinstance(status_clause, codex.operator.NOT)
    assert isinstance(status_clause.value, codex.operator.IS)
    assert status_clause.value.value is None


def test_parser_requires_registered_field_types() -> None:
    parser = Parser()
    query = {"missing": "value"}
    types_map = {"known": str}

    with pytest.raises(Exception, match="Type of parameter missing"):
        list(parser(query, types_map))


def test_parser_like_variants_and_negations() -> None:
    parser = Parser()
    query = "fuzzy=~~foo&fuzzy_neg=!~~bar&case=~Baz&case_neg=!~Qux&not_equal=!plain"
    types_map = {
        "fuzzy": str,
        "fuzzy_neg": str,
        "case": str,
        "case_neg": str,
        "not_equal": str,
    }

    clauses = {name: clause for name, clause in parser(query, types_map)}

    assert isinstance(clauses["fuzzy"], codex.operator.LIKE)
    assert clauses["fuzzy"].value == "foo"
    assert clauses["fuzzy"].case_sensitive is False

    fuzzy_neg = clauses["fuzzy_neg"]
    assert isinstance(fuzzy_neg, codex.operator.NOT)
    assert isinstance(fuzzy_neg.value, codex.operator.LIKE)
    assert fuzzy_neg.value.value == "bar"

    assert isinstance(clauses["case"], codex.operator.LIKE)
    assert clauses["case"].case_sensitive is True

    case_neg = clauses["case_neg"]
    assert isinstance(case_neg, codex.operator.NOT)
    assert isinstance(case_neg.value, codex.operator.LIKE)
    assert case_neg.value.case_sensitive is True

    not_equal = clauses["not_equal"]
    assert isinstance(not_equal, codex.operator.NOT)
    assert isinstance(not_equal.value, codex.operator.EQ)
    assert not_equal.value.value == "plain"


def test_parser_handles_open_interval_bounds() -> None:
    parser = Parser()
    query = collections.OrderedDict(
        [
            ("upper", "(,10)"),
            ("lower", "[5,]"),
        ]
    )
    types_map = {
        "upper": float,
        "lower": float,
    }

    clauses = list(parser(query, types_map))

    _, upper_clause = clauses[0]
    assert isinstance(upper_clause, codex.operator.RANGE)
    left, right = upper_clause.value
    assert left is None
    assert isinstance(right, codex.operator.LT)
    assert right.value == 10.0

    _, lower_clause = clauses[1]
    assert isinstance(lower_clause, codex.operator.RANGE)
    lower_left, lower_right = lower_clause.value
    assert isinstance(lower_left, codex.operator.GE)
    assert lower_right is None


def test_parser_rejects_interval_for_unsupported_type() -> None:
    parser = Parser()
    query = {"name": "[1,2]"}
    types_map = {"name": str}

    with pytest.raises(TypeError, match="Interval cannot be calculated"):
        list(parser(query, types_map))


def test_parser_rejects_interval_with_wrong_member_count() -> None:
    parser = Parser()
    query = {"score": "[1]"}
    types_map = {"score": int}

    with pytest.raises(ValueError, match="Range must contain strictly two members"):
        list(parser(query, types_map))


def test_parser_casts_boolean_values_and_validates_input() -> None:
    parser = Parser()
    types_map = {"is_active": bool}

    clauses = {name: clause for name, clause in parser("is_active=false", types_map)}
    assert isinstance(clauses["is_active"], codex.operator.EQ)
    assert clauses["is_active"].value is False

    with pytest.raises(ValueError, match="Unable to cast"):
        list(parser("is_active=maybe", types_map))


def test_parser_skips_none_and_validates_query_types() -> None:
    parser = Parser()
    query: dict[str, str | None] = {
        "status": None,
        "flag": "true",
    }
    types_map = {
        "status": str,
        "flag": bool,
    }

    clauses = list(parser(query, types_map))
    assert len(clauses) == 1
    assert clauses[0][0] == "flag"

    with pytest.raises(ValueError, match="specify name"):
        list(parser("valueonly", types_map))

    with pytest.raises(ValueError, match="Query mast be string or mapping"):
        list(parser(42, types_map))


def test_parser_returns_none_when_no_handler_matches() -> None:
    class EmptyHandlerParser(Parser):
        def _pattern_handler_map(
            self, types_map: collections.abc.Mapping[str, type]
        ) -> collections.abc.Mapping[re.Pattern[str], collections.abc.Callable[[str, str], typing.Any]]:
            return {}

    parser = EmptyHandlerParser()
    result = parser._parse_filter_param("field", "value", {"field": str})
    assert result is None
