from __future__ import annotations

import unittest
from dataclasses import dataclass
from datetime import datetime

from Aspidites._vendor.apm import *


class BasicUseCases(unittest.TestCase):

    def test_dict(self):
        self.assertTrue(match(
            {"foo": 1, "bar": 2, "baz": 3, "quux": 4},
            {"foo": 1}
        ))
        self.assertFalse(match(
            {"foo": 1, "bar": 2, "baz": 3, "quux": 4},
            Strict({"foo": 1})
        ))
        positive_number = InstanceOf(int) & Check(lambda v: v >= 0)
        self.assertTrue(match(
            {"foo": 1, "bar": 2},
            Strict({"foo": positive_number, "bar": positive_number})
        ))

    def test_tuple(self):
        self.assertTrue(match(
            (1, 2),
            (..., ...)
        ))

    def test_strict(self):
        self.assertTrue(match(1, 1.0))
        self.assertTrue(match(1.0, Strict(1.0)))
        self.assertTrue(match(1, Strict(1)))
        self.assertFalse(match(1, Strict(1.0)))

    def test_truish(self):
        self.assertTrue(match(1, IsTruish))
        self.assertTrue(match(2.9, IsTruish))
        self.assertFalse(match("", IsTruish))

    def test_length(self):
        self.assertTrue(match([], Length(exactly=0)))
        self.assertTrue(match(('abc',), Length(exactly=1)))
        self.assertTrue(match('abc', Length(exactly=3)))
        self.assertTrue(match('abc', Length(at_least=1, at_most=4)))
        self.assertFalse(match('abc', Length(at_least=4, at_most=10)))
        self.assertTrue(match('abcde', Length(at_least=4, at_most=5)))
        self.assertFalse(match('abcdef', Length(at_least=4, at_most=5)))

    def test_contains(self):
        self.assertTrue(match([1, 2, 3], Contains(2) & Contains(3)))
        self.assertTrue(match((1, 2, 3), Contains(2) & Contains(3)))
        self.assertTrue(match({1, 2, 3}, Contains(2) & Contains(3)))
        self.assertTrue(match({1: 'a', 2: 'b', 3: 'c'}, Contains(2) & Contains(3)))
        self.assertTrue(match("quux", Contains('uu')))

    def test_regex(self):
        self.assertTrue(match(
            "Hello, World!",
            Regex("[A-Z][a-z]+, [A-Z][a-z]+!")
        ))
        self.assertFalse(match(
            "Hello, World! How are you today?",
            Regex("[A-Z][a-z]+, [A-Z][a-z]+!")
        ))
        self.assertTrue(match(
            "Hello, World! How are you today?",
            Regex("[A-Z][a-z]+, [A-Z][a-z]+!.*")
        ))

    def test_remaining(self):
        self.assertTrue(match(
            [1, 2, 3, 4],
            [1, 2, 3, Remaining(InstanceOf(int), at_least=1)]))
        self.assertTrue(match(
            [1, 2, 3],
            [1, 2, 3, Remaining(InstanceOf(int))]))
        self.assertTrue(match(
            [1, 2, 3, 4, 5],
            [1, 2, 3, Remaining(InstanceOf(int) & Between(1, 10))]))
        self.assertFalse(match(
            [1, 2, 3, 4, 5],
            [1, 2, 3, Remaining(InstanceOf(int) & Between(1, 3))]))
        self.assertFalse(match(
            [1, 2, 3, 4],
            [1, 2, 3, Remaining(InstanceOf(int), at_least=2)]))

    def test_remaining_with_range(self):
        self.assertTrue(match(
            range(1, 5),
            [1, 2, 3, Remaining(InstanceOf(int), at_least=1)]))
        self.assertTrue(match(
            range(1, 4),
            [1, 2, 3, Remaining(InstanceOf(int))]))
        self.assertTrue(match(
            range(1, 5),
            [1, 2, 3, Remaining(InstanceOf(int) & Between(1, 10))]))
        self.assertFalse(match(
            range(1, 5),
            [1, 2, 3, Remaining(InstanceOf(int) & Between(1, 3))]))
        self.assertFalse(match(
            range(1, 5),
            [1, 2, 3, Remaining(InstanceOf(int), at_least=2)]))

    def test_capture(self):
        result = match(
            {
                "This": "Is A Rather Complex Beast",
                "Created-At": "Sun Jan  3 04:08:57 CET 2021",
                "User": {
                    "UserId": 102384,
                    "FirstName": "Jane",
                    "LastName": "Doe",
                }
            },
            {
                "User": Capture({
                    "FirstName": Capture(..., name="first_name"),
                    "LastName": Capture(..., name="last_name"),
                }, name="user")
            }
        )
        self.assertTrue(result)
        self.assertEqual("Jane", result['first_name'])
        self.assertEqual("Doe", result['last_name'])
        self.assertEqual(102384, result['user']['UserId'])

    def test_remaining_with_capture(self):
        result = match(
            [1, 2, 3, 4],
            [1, 2, Capture(Remaining(...), name="tail")]
        )
        self.assertTrue(result)
        self.assertEqual([3, 4], result['tail'])

    def test_and(self):
        self.assertFalse(match(
            0,
            Between(0, 1) & Between(1, 2)
        ))
        self.assertTrue(match(
            1,
            Between(0, 1) & Between(1, 2)
        ))
        self.assertFalse(match(
            2,
            Between(0, 1) & Between(1, 2)
        ))

    def test_or(self):
        self.assertTrue(match(
            0,
            Between(0, 1) | Between(1, 2)
        ))
        self.assertTrue(match(
            1,
            Between(0, 1) | Between(1, 2)
        ))
        self.assertTrue(match(
            2,
            Between(0, 1) | Between(1, 2)
        ))

    def test_xor(self):
        self.assertTrue(match(
            0,
            Between(0, 1) ^ Between(1, 2)
        ))
        self.assertFalse(match(
            1,
            Between(0, 1) ^ Between(1, 2)
        ))
        self.assertTrue(match(
            2,
            Between(0, 1) ^ Between(1, 2)
        ))

    def test_each(self):
        self.assertTrue(match([1, 2, 3], Each(InstanceOf(int))))
        self.assertFalse(match([1, 2.0, 3], Each(InstanceOf(int))))

    def test_each_item(self):
        pattern = EachItem(Regex("[a-z]+"), InstanceOf(int))
        self.assertTrue(match({"a": 1, "b": 2}, pattern))
        self.assertFalse(match({"_a": 1, "b": 2}, pattern))
        self.assertFalse(match({"a": 7.5}, pattern))

    def test_transformed(self):
        self.assertTrue(match("value", Transformed(len, 5)))

    def test_arguments(self):
        # noinspection PyUnusedLocal
        def f(x: int, y: str):
            pass

        # noinspection PyUnusedLocal
        def g(x):
            pass

        # noinspection PyUnusedLocal
        def h(x, y: int, m: str, n: str):
            pass

        self.assertTrue(match(f, Arguments(int, str)))
        self.assertFalse(match(g, Arguments(int)))
        self.assertTrue(match(h, Arguments(None, int, Remaining(str))))

    def test_keyword_arguments(self):
        # noinspection PyUnusedLocal
        def f(x: int, y: bool):
            pass

        # noinspection PyUnusedLocal
        def g(*, x: int):
            pass

        self.assertTrue(match(f, Arguments(x=int)))
        self.assertFalse(match(f, Strict(Arguments(x=int))))
        self.assertTrue(match(f, Strict(Arguments(x=int, y=bool))))
        self.assertTrue(match(g, Arguments(x=int)))

    def test_returns(self):
        def f() -> int:
            pass

        self.assertTrue(match(f, Returns(int)))
        self.assertFalse(match(f, Returns(float)))

    def test_at(self):
        record = {
            "foo": {
                "bar": {
                    "quux": {
                        "value": "deeply nested"
                    }
                }
            }
        }

        result = match(record, At("foo.bar.quux", {"value": Capture(..., name="value")}))
        self.assertTrue(result)
        self.assertEqual("deeply nested", result['value'])

        result = match(record, At(['foo', 'bar', 'quux'], {"value": Capture(..., name="value")}))
        self.assertTrue(result)
        self.assertEqual("deeply nested", result['value'])

        self.assertFalse(match(record, At("foo.bar.quux.baz", InstanceOf(int))))

    def test_string(self):
        path = "https://somehost/foo=1/bar=2/"
        result = match(path, String(
            Capture(OneOf("https", "http"), name="protocol"),
            "://",
            Capture(Regex("[a-zA-Z_-]+"), name="host"),
            Regex("/+"),
            "foo=",
            Capture(Regex("[^=/]+"), name="foo"),
            "/",
            "bar=",
            Capture(Regex("[^=/]+"), name="bar"),
            Regex("/*"),
        ))
        self.assertTrue(result)
        self.assertEqual("https", result['protocol'])
        self.assertEqual("somehost", result['host'])
        self.assertEqual("1", result['foo'])
        self.assertEqual("2", result['bar'])

    def test_string_argresult(self):
        path = "https://somehost/foo=1/bar=2/"

        result = match(path, String(
            Capture(OneOf("https", "http"), name="protocol"),
            "://",
            Capture(Regex("[a-zA-Z_-]+"), name="host"),
            Regex("/+"),
            "foo=",
            Capture(Regex("[^=/]+"), name="foo"),
            "/",
            "bar=",
            Capture(Regex("[^=/]+"), name="bar"),
            Regex("/*"),
        ))
        self.assertTrue(result)
        self.assertEqual("https", result.protocol)
        self.assertEqual("somehost", result.host)
        self.assertEqual("1", result.foo)
        self.assertEqual("2", result.bar)

    def test_string_with_at_capture(self):
        path = "https://somehost/foo=1/bar=2/"

        result = match(path, String(
            'protocol' @ OneOf("https", "http"),
            "://",
            'host' @ Regex("[a-zA-Z_-]+"),
            Regex("/+"),
            "foo=",
            'foo' @ Regex("[^=/]+"),
            "/",
            "bar=",
            'bar' @ Regex("[^=/]+"),
            Regex("/*"),
        ))
        self.assertTrue(result)
        self.assertEqual("https", result.protocol)
        self.assertEqual("somehost", result.host)
        self.assertEqual("1", result.foo)
        self.assertEqual("2", result.bar)

    def test_is_type(self):
        self.assertTrue(match(1, IsNumber))
        self.assertTrue(match(1.0, IsNumber))
        self.assertFalse(match(True, IsNumber))

    def test_object(self):
        request = {
            "api_version": "v1",
            "job": {
                "run_at": "2020-08-27 14:09:30",
                "command": "echo 'booya'",
            }
        }

        # noinspection PyUnresolvedReferences
        result = match(request, Items(
            api_version="v1",
            job=Items(
                run_at=Transformed(datetime.fromisoformat, 'time' @ _),
            ) & OneOf(
                Items(command='command' @ InstanceOf(str)),
                Items(spawn='container' @ InstanceOf(str)),
            )
        ))
        self.assertTrue(result)
        self.assertFalse('container' in result)
        self.assertEqual(result['time'], datetime(2020, 8, 27, 14, 9, 30))
        self.assertEqual(result['command'], "echo 'booya'")

    def test_attrs(self):
        @dataclass
        class Thing:
            foo: str
            bar: int

        self.assertTrue(match(Thing('xyz', 4711), Attrs(foo='xyz')))
        self.assertFalse(match(Thing('xyz', 4711), Strict(Attrs(foo='xyz'))))
        self.assertTrue(match(Thing('xyz', 4711), Attrs(foo=InstanceOf(str), bar=InstanceOf(int))))
        self.assertTrue(match(Thing('xyz', 4711), Strict(Attrs(foo=InstanceOf(str), bar=InstanceOf(int)))))

    def test_transformed_exception(self):
        request = {
            "api_version": "v1",
            "job": {
                "run_at": "junk",
                "command": "echo 'booya'",
            }
        }

        # noinspection PyUnresolvedReferences
        self.assertFalse(match(request, Items(
            job=Items(
                run_at=Transformed(datetime.fromisoformat, 'time' @ _),
            )
        )))

    def test_splat_result(self):
        def f(**kwargs):
            dct = {}
            for k, v in kwargs.items():
                dct[k] = v
            return dct

        result = match({'a': 1, 'b': 2, 'c': 3}, Items(a=_ >> 'x', b=_ >> 'y', c=_ >> 'z'))
        self.assertTrue(result)
        r = f(**result)
        self.assertEqual({'x': 1, 'y': 2, 'z': 3}, r)

    def test_maybe(self):
        pattern = Items(a=...) & Maybe(Items(b=_ >> 'capture'))

        result = match({'a': 3}, pattern)
        self.assertTrue(result)
        self.assertFalse('capture' in result)
        result = match({'a': 3, 'b': 5}, pattern)
        self.assertTrue(result)
        self.assertTrue('capture' in result)
        self.assertEqual(5, result['capture'])

    def test_dataclasses(self):
        @dataclass
        class User:
            first_name: str
            last_name: str

        @dataclass
        class LoggedInUser(User):
            display_name: str

        result = match(User("Jane", "Doe"), User("Jane", _ >> 'last_name'))
        self.assertTrue(result)
        self.assertEqual("Doe", result['last_name'])

        result = match(LoggedInUser("John", "Doe", "johndoe"), User(_, _ >> 'last_name'))
        self.assertTrue(result)
        self.assertEqual("Doe", result['last_name'])

    def test_bind(self):
        result = {}
        if match({'a': 1, 'b': 2}, {'a': _ >> 'foo'}).bind(result):
            self.assertEqual(1, result['foo'])
        self.assertEqual({'foo': 1}, result)

    def test_extract(self):
        # noinspection PyUnusedLocal
        def f(arg: str) -> float:
            pass

        # noinspection PyUnusedLocal
        def g(a: int, b: int):
            pass

        # noinspection PyUnresolvedReferences
        result = match(f, Arguments('arg_types' @ Many(...)) & Returns('return_type' @ _))
        self.assertTrue(result)
        self.assertEqual([str], result.arg_types)
        self.assertEqual(float, result.return_type)

        self.assertTrue(match(f, Arguments(_)))
        self.assertFalse(match(g, Arguments(_)))
        self.assertFalse(match(f, Arguments(_, _)))
        self.assertTrue(match(g, Arguments(_, _)))

    def test_strict_match(self):
        self.assertTrue(match({'a': 3}, {'a': 3.0}))
        self.assertFalse(match({'a': 3}, {'a': 3.0}, strict=True))

    def test_groups(self):
        # noinspection PyUnresolvedReferences
        result = match([1, 2, 3], ['a' @ _, 'b' @ _, 'c' @ _])
        self.assertTrue(result)
        self.assertEqual(3, len(result))
        self.assertEqual({'a': 1, 'b': 2, 'c': 3}, result.groups())

    def test_subclass_of(self):
        result = match(bool, SubclassOf(int))
        self.assertTrue(result)

    def test_is_truish(self):
        self.assertTrue(match([0], IsTruish))
        self.assertTrue(match([], Not(IsTruish)))
        self.assertFalse(match([], IsTruish))

    def test_range(self):
        self.assertTrue(match([0, 1, 2], range(0, 3)))
        self.assertFalse(match([0, 1, 2], range(0, 3), strict=True))
        self.assertTrue(match(range(0, 3), range(0, 3), strict=True))
        self.assertFalse(match(range(0, 3), range(0, 2), strict=True))
        self.assertFalse(match(range(0, 2), range(0, 3), strict=True))
