# SPDX-License-Identifier: LGPL-3.0-only
# Copyright (C) 2023 Michał Góral.

import re
from dataclasses import dataclass
from datetime import date, datetime
from pathlib import Path
from typing import Optional

import pytest

from cmplib import *


def tznow() -> datetime:
    """Return timezone-aware current datetime."""
    return datetime.now().astimezone()


def test_truish():
    assert True == Truish()
    assert 1 == Truish()
    assert "foo" == Truish()

    assert False != Truish()
    assert 0 != Truish()
    assert "" != Truish()


def test_eq():
    assert "abc" == Eq("abc")
    assert "" == Eq("")
    assert "abc" == Eq(And(Contains("b"), Len(3)))

    assert "" != Eq("abc")
    assert "abc" != Eq("")
    assert None != Eq("")
    assert 1 != Eq("")


def test_ne():
    assert "" == Ne("abc")
    assert "abc" == Ne("")
    assert None == Ne("")
    assert 1 == Ne("")
    assert "abc" == Ne(And(Contains("b"), Len(4)))

    assert "abc" != Ne("abc")
    assert "" != Ne("")


def test_gt():
    assert 1 == Gt(0)
    assert 0 == Gt(-1)
    assert 0 != Gt(0)
    assert -1 != Gt(0)


def test_ge():
    assert 1 == Ge(0)
    assert 0 == Ge(0)
    assert -1 != Ge(0)


def test_lt():
    assert 1 != Lt(0)
    assert 0 != Lt(0)
    assert -1 == Lt(0)


def test_le():
    assert 1 != Le(0)
    assert 0 == Le(0)
    assert -1 == Le(0)


def test_len():
    assert [] == Len(0)
    assert [1, 2, 3, "aaa"] == Len(4)
    assert [1, 2, 3, "aaa"] == Len(And(Gt(0), Lt(5)))
    assert [1, 2, 3, "aaa"] != Len(And(Gt(0), Lt(4)))


def test_values():
    assert [] == Values(1, at_least=0)
    assert [0, 2, 3, 1, 2] == Values(1)
    assert [0, 2, 3, 1, 2] == Values(2, at_least=2)
    assert [0, 2, 3, 1, 2] == Values(Gt(1), at_least=2)
    assert [0, 2, 3, 1, 2] == Values(5, at_least=0)
    assert [0, 2, 3, 1, 2] == Values(1, at_least=0)
    assert [0, 2, 3, 1, 2] == Values(Gt(3), at_least=0)

    assert [] != Values(1)
    assert [0, 2, 3, 1, 2] != Values(1, at_least=2)
    assert [0, 2, 3, 1, 2] != Values(Gt(2), at_least=2)


def test_contains():
    assert [1, 2, 3] == Contains(1)
    assert [1, 2, 3] == Contains(Or("a", 2, "b"))

    assert [1, 2, 3] != Contains(0)
    assert [1, 2, 3] != Contains(Eq(0))


def test_not():
    assert False == Not(Truish())
    assert True != Not(Truish())
    assert 1 == Not(Gt(1))
    assert 1 == Not(2)


def test_keyeq():
    d = {"foo": "bar", "baz": "blah"}

    assert d == KeyEq("foo", "bar")
    assert d == KeyEq("baz", "blah")

    assert d != KeyEq("foo", "barr")
    assert d != KeyEq("foo", "")
    assert d != KeyEq("foo", "blah")
    assert d != KeyEq("baz", "bar")

    lst = ["foo", "bar"]

    assert lst == KeyEq(0, "foo")
    assert lst == KeyEq(1, "bar")
    assert lst == KeyEq(-1, "bar")

    assert lst != KeyEq(0, "bar")
    assert lst != KeyEq(100, "bar")


def test_keyeq_composability():
    d = {"foo": "bar", "baz": "blah"}
    assert d == KeyEq("foo", Not(IsEmpty()))
    assert d == KeyEq("foo", And(Not(IsEmpty()), Truish(), "bar", Not(Not(Truish()))))
    assert d == KeyEq("foo", Not(IsEmpty()) & Truish() & "bar")
    assert d == KeyEq("foo", Not(Contains("h")) & Contains("b") & Contains("a"))
    assert d != KeyEq("foo", And(Not(IsEmpty()), Truish(), "fake"))
    assert d != KeyEq("foo", Not(IsEmpty()) & Truish() & "fake")
    assert d != KeyEq("foo", IsEmpty())


def test_attreq():
    @dataclass
    class Foo:
        foo: str = "bar"
        baz: str = "blah"

    o = Foo()

    assert o == AttrEq("foo", "bar")
    assert o == AttrEq("baz", "blah")

    assert o != AttrEq("foo", "barr")
    assert o != AttrEq("foo", "")
    assert o != AttrEq("foo", "blah")
    assert o != AttrEq("baz", "bar")


def test_attreq_composability():
    @dataclass
    class Foo:
        foo: str = "bar"
        baz: str = "blah"

    o = Foo()

    assert o == AttrEq("foo", Not(IsEmpty()))
    assert o == AttrEq("foo", And(Not(IsEmpty()), Truish(), "bar", Not(Not(Truish()))))
    assert o == AttrEq("foo", Not(IsEmpty()) & Truish() & "bar")
    assert o != AttrEq("foo", And(Not(IsEmpty()), Truish(), "fake"))
    assert o != AttrEq("foo", Not(IsEmpty()) & Truish() & "fake")
    assert o != AttrEq("foo", IsEmpty())


def test_and():
    l = [1, 2, 3]
    d = {"foo": "bar", "baz": "blah"}

    assert l == And(Contains(1), Contains(2))
    assert l == Contains(1) & Contains(2)
    assert d == And(KeyEq("foo", "bar"), KeyEq("baz", Not(IsEmpty())))
    assert d == KeyEq("foo", "bar") & KeyEq("baz", Not(IsEmpty()))

    assert l != And(Contains(1), Contains(2), Contains(3), Contains(4))
    assert l != Contains(1) & Contains(2) & Contains(3) & Contains(4)


def test_or():
    l = [1, 2, 3]
    d = {"foo": "bar", "baz": "blah"}

    assert l == Or(Contains(5), Contains(2))
    assert l == Contains(5) | Contains(2)
    assert d == Or(KeyEq("foo", "blah"), KeyEq("baz", "blah"))
    assert d == KeyEq("foo", "blah") | KeyEq("baz", "blah")

    assert l != Or(Contains(5), Contains(6), Contains(7), Contains(8))
    assert l != Contains(5) | Contains(6) | Contains(7) | Contains(8)


def test_empty():
    assert [] == IsEmpty()
    assert "" == IsEmpty()
    assert dict() == IsEmpty()
    assert set() == IsEmpty()
    assert tuple() == IsEmpty()

    assert [1] != IsEmpty()
    assert "a" != IsEmpty()
    assert dict(a=1) != IsEmpty()
    assert set([1]) != IsEmpty()
    assert tuple([1]) != IsEmpty()


def test_isisodate():
    now = tznow()
    now_naive = datetime.now()
    today_naive = date.today()

    assert now == IsIsoDateTime()
    assert now_naive == IsIsoDateTime()
    assert today_naive == IsIsoDateTime()
    assert now.isoformat() == IsIsoDateTime()
    assert now_naive.isoformat() == IsIsoDateTime()
    assert today_naive.isoformat() == IsIsoDateTime()

    assert "" != IsIsoDateTime()
    assert "2022" != IsIsoDateTime()
    assert "2022-03" != IsIsoDateTime()
    assert 11 != IsIsoDateTime()
    assert [now] != IsIsoDateTime()
    assert [now.isoformat()] != IsIsoDateTime()
    assert now.isoformat() + ";" != IsIsoDateTime()


def test_istimestamp():
    now = tznow().timestamp()
    today_naive = date.today()

    assert 0 == CanBeTimestamp()
    assert "0" == CanBeTimestamp()
    assert "0.123" == CanBeTimestamp()
    assert now == CanBeTimestamp()

    assert "" != CanBeTimestamp()
    assert "nan" != CanBeTimestamp()
    assert "2022-03" != CanBeTimestamp()
    assert [now] != CanBeTimestamp()
    assert str(now) + ";" != CanBeTimestamp()
    assert today_naive != CanBeTimestamp()
    assert tznow() != CanBeTimestamp()


def test_is():
    l1 = []
    l2 = []

    assert None == Is(None)
    assert l1 == Is(l1)
    assert l2 == Is(l2)

    assert l1 != Is(l2)

    # apparently id("abc") == id("abc") as Python caches constructed static
    # strings, so we must employ a trick to ensure that the same strings
    # actually differ
    assert "abc" != Is("abcd"[:-1])


def test_isnone():
    assert None == IsNone()
    assert 0 == Not(IsNone())

    assert [] != IsNone()
    assert "" != IsNone()
    assert dict() != IsNone()
    assert set() != IsNone()
    assert tuple() != IsNone()


def test_isinstance():
    now = tznow()

    assert 1 == IsInstance(int)
    assert now == IsInstance(datetime)
    assert now == IsInstance(date)

    assert 1 != IsInstance(bool)
    assert 1 != IsInstance(str)


def test_unordered():
    assert [1, 2, 3] == Unordered(1, 2, 3)
    assert [1, 2, 3] == Unordered(3, 2, 1)
    assert [1, 2, 3] == Unordered(Eq(3), Or(5, 6, 7, 9, 2), 1)
    assert [1, 2, 3] == Unordered(Truish(), Truish(), Truish())
    assert [] == Unordered()

    assert [1, 2, 3] != Unordered(4)
    assert [1, 2, 3] != Unordered(3, 2, 1, 1)
    assert [1, 2, 3] != Unordered(3, 2, 1, 1)
    assert [1, 2, 3] != Unordered(3, 2)
    assert [1, 2, 3] != Unordered()
    assert [1, 2, 3] != Unordered("1", "2", "3")
    assert [1, 2, 3] != Unordered(Truish(), Truish())
    assert [1, 2, 3] != Unordered(Truish(), Truish(), Truish(), Truish())


def test_skip():
    assert [1, 2, 3] == [SKIP, SKIP, SKIP]
    assert [1, 2, 3] == [1, SKIP, SKIP]


def test_each():
    assert [1, 2, 3] == Each(Truish())
    assert [1, 1, 1] == Each(1)
    assert [1, 2, 3] == Each(Not(0))

    assert [0, 2, 3] != Each(Truish())
    assert [1, 1, 1] != Each(2)
    assert [0, 2, 3] != Each(Not(0))


def test_fn():
    assert 1 == Fn(lambda x: x == 1)
    assert "1" == Fn((lambda x: x), coerce=True)

    assert 1 != Fn(lambda x: x == 0)
    assert "1" != Fn(lambda x: x)
    assert "1" != Fn((lambda x: ""), coerce=True)


def test_unique():
    assert 1 == IsUnique("cache-1")
    assert 1 == IsUnique("cache-2")
    assert 2 == IsUnique("cache-1")

    assert 1 != IsUnique("cache-1")

    IsUnique.clear("cache-1")
    assert 1 == IsUnique("cache-1")
    assert 1 != IsUnique("cache-2")

    IsUnique.clear()
    assert 1 == IsUnique("cache-1")
    assert 1 == IsUnique("cache-2")


def test_unique_default_cache_id():
    assert 1 == IsUnique(1)
    assert 1 == IsUnique()
    assert 1 == IsUnique()
    assert [1, 2, 3, 4] == Each(IsUnique())
    assert [1, 2, 3, 4, 1, 1, 7] != Each(IsUnique())

    U = IsUnique()
    assert 1 == U
    assert 1 != U


def test_object():
    @dataclass
    class Foo:
        foo: int = 1
        bar: int = 2
        baz: str = "s"
        o: Optional["Foo"] = None

    assert Foo() == Object(foo=1, bar=Ge(2))
    assert Foo(o=Foo()) == Object(foo=1, o=Object(bar=2, o=None))

    assert Foo() != Object(dict, foo=1)
    assert Foo() != Object(nonexisting=1)


def test_dictfields():
    d = {"foo": 1, "bar": 2, "baz": "s", "o": {"foo": 1}}
    assert d == DictFields()
    assert d == DictFields(foo=1, bar=Ge(2))
    assert d == DictFields(foo=1, o=DictFields(foo=1))

    assert d != DictFields(foo=1, o=DictFields(foo=2))
    assert d != DictFields(foo=1, o=DictFields(nonexisting=2))


def test_items():
    lst = ["foo", "bar", "baz", "blah"]
    assert lst == Items()
    assert lst == Items((0, "foo"), (-1, "blah"))
    assert lst == Items((-1, "blah"), (0, "foo"))
    assert lst == Items((0, "foo"), (-2, "baz"), (-1, "blah"))
    assert lst == Items((0, "foo"), (0, "foo"))

    assert lst != Items((0, "foo"), (-1, "blah2"))
    assert lst != Items((0, "foo"), (-2, "blah"))
    assert lst != Items((0, "foo"), (1, "foo"))
    assert lst != Items((0, "foo"), (100, "foo"))


def test_glob():
    assert "" == Glob("*")
    assert "foo" == Glob("foo")
    assert "foo" == Glob("f*")
    assert "foo" == Glob("*")
    assert ["foo", "bar"] == [Glob("f*"), Glob("b*")]
    assert ["foo", "bar"] == Contains(Glob("b*"))
    assert 11 == Glob("1*", coerce=True)
    assert Path("/foo/bar") == Glob("/foo/*", coerce=True)
    assert "Foo" == Glob("foo", case=False)
    assert "foo" == Glob("Foo", case=False)
    assert Path("/Foo/bar") == Glob("/foo/*", coerce=True, case=False)
    assert Path("/foo/bar") == Glob("/Foo/*", coerce=True, case=False)

    assert "afoo" != Glob("foo")
    assert "foo" != Glob("f")
    assert "foo" != Glob("fool")
    assert "foo" != Glob("fooo*")
    assert ["foo"] != Glob("*")
    assert True != Glob("*")
    assert False != Glob("*")
    assert 1 != Glob("*")
    assert Path("/foo/bar") != Glob("/foo/*")
    assert "Foo" != Glob("foo")
    assert "foo" != Glob("Foo")


def test_re():
    assert "" == Re(".*")
    assert "foo" == Re("foo")
    assert "foo" == Re("(foo)")
    assert "foo" == Re("(?:foo)")
    assert "foo" == Re("(?P<name>foo)")
    assert "foo bar baz" == Re("^foo.*z$")
    assert "to foo or to not foo" == Re(".*foo")
    assert "foo" == Re("foo*")
    assert "foo" == Re(".*")
    assert ["foo", "bar"] == [Re("f.*"), Re("b.*")]
    assert ["foo", "bar"] == Contains(Re("b.*"))
    assert 11 == Re(r"\d+", coerce=True)
    assert Path("/foo/bar") == Re("/foo/.*", coerce=True)
    assert "Foo\n" == Re("foo.", flags=re.IGNORECASE | re.DOTALL)

    assert "afoo" != Re("foo")
    assert "foo" != Re(" foo")
    assert "foo" != Re("fool")
    assert "to foo or to not foo" != Re("foo")
    assert "to foo or to not foo" != Re("^fo{1,2}")
    assert True != Re(".*")
    assert False != Re(".*")
    assert 1 != Re(".*")
    assert 1 != Re(r"\d*")
    assert "Foo" != Re("foo")


@pytest.mark.parametrize("pat", ["*", "(.*"])
def test_re_invalid_patterns(pat):
    with pytest.raises(re.error):
        # Make sure that exception occurs during Re initialisation, not during
        # comparison
        Re(pat)
