"""
    Test functions for the matching module
"""
import discord
import pytest
import random
import matchy.matching as matching
import matchy.state as state
import copy
import itertools
from datetime import datetime, timedelta


@pytest.fixture(autouse=True)
def clean_state():
    """Ensure every single one of these tests has a clean state"""
    state.State = state._State(state._EMPTY_DICT)


def test_protocols():
    """Verify the protocols we're using match the discord ones"""
    assert isinstance(discord.Member, matching.Member)
    assert isinstance(discord.Guild, matching.Guild)
    assert isinstance(discord.Role, matching.Role)
    assert isinstance(Member, matching.Member)
    assert isinstance(Role, matching.Role)


class Role():
    def __init__(self, id: int):
        self._id = id

    @property
    def id(self) -> int:
        return self._id

    @property
    def name(self) -> str:
        pass


class Member():
    def __init__(self, id: int, roles: list[Role] = []):
        self._id = id
        self._roles = roles

    @property
    def mention(self) -> str:
        return f"<@{self._id}>"

    @property
    def display_name(self) -> str:
        return f"{self._id}"

    @property
    def roles(self) -> list[Role]:
        return self._roles

    @roles.setter
    def roles(self, roles: list[Role]):
        self._roles = roles

    @property
    def id(self) -> int:
        return self._id


def members_to_groups_validate(matchees: list[Member], per_group: int):
    """Inner function to validate the main output of the groups function"""
    groups = matching.members_to_groups(matchees, per_group)

    # We should always have one group
    assert len(groups)

    # Log the groups to history
    # This will validate the internals
    state.State.log_groups(groups)

    # Ensure each group contains within the bounds of expected members
    for group in groups:
        if len(matchees) >= per_group:
            assert len(group) >= per_group
        else:
            assert len(group) == len(matchees)
        assert len(group) < per_group*2  # TODO: We could be more strict here

    return groups


@pytest.mark.parametrize("matchees, per_group", [
    # Simplest test possible
    ([Member(1)], 1),

    # More requested than we have
    ([Member(1)], 2),

    # A selection of hyper-simple checks to validate core functionality
    ([Member(1)] * 100, 3),
    ([Member(1)] * 12, 5),
    ([Member(1)] * 11, 2),
    ([Member(1)] * 356, 8),
], ids=['single', "larger_groups", "100_members", "5_group", "pairs", "356_big_groups"])
def test_members_to_groups_no_history(matchees, per_group):
    """Test simple group matching works"""
    members_to_groups_validate(matchees, per_group)


def items_found_in_lists(list_of_lists, items):
    """validates if any sets of items are found in individual lists"""
    for sublist in list_of_lists:
        if all(item in sublist for item in items):
            return True
    return False


@pytest.mark.parametrize("history_data, matchees, per_group, checks", [
    # Slightly more difficult test
    (
        # Describe a history where we previously matched up some people and ensure they don't get rematched
        [
            {
                "ts": datetime.now() - timedelta(days=1),
                "groups": [
                    [Member(1), Member(2)],
                    [Member(3), Member(4)],
                ]
            }
        ],
        [
            Member(1),
            Member(2),
            Member(3),
            Member(4),
        ],
        2,
        [
            lambda groups: not items_found_in_lists(
                groups, [Member(1), Member(2)]),
            lambda groups: not items_found_in_lists(
                groups, [Member(3), Member(4)])
        ]
    ),
    # Feed the system an "impossible" test
    # The function should fall back to ignoring history and still give us something
    (
        [
            {
                "ts": datetime.now() - timedelta(days=1),
                "groups": [
                    [
                        Member(1),
                        Member(2),
                        Member(3)
                    ],
                    [
                        Member(4),
                        Member(5),
                        Member(6)
                    ],
                ]
            }
        ],
        [
            Member(1, [Role(1), Role(2), Role(3), Role(4)]),
            Member(2, [Role(1), Role(2), Role(3), Role(4)]),
            Member(3, [Role(1), Role(2), Role(3), Role(4)]),
            Member(4, [Role(1), Role(2), Role(3), Role(4)]),
            Member(5, [Role(1), Role(2), Role(3), Role(4)]),
            Member(6, [Role(1), Role(2), Role(3), Role(4)]),
        ],
        3,
        [
            # Nothing specific to validate
        ]
    ),
    # Specific test pulled out of the stress test
    (
        [
            {
                "ts": datetime.now() - timedelta(days=4),
                "groups": [
                    [Member(i) for i in [1, 2, 3, 4, 5, 6,
                                         7, 8, 9, 10, 11, 12, 13, 14, 15]]
                ]
            },
            {
                "ts": datetime.now() - timedelta(days=5),
                "groups": [
                    [Member(i) for i in [1, 2, 3, 4, 5, 6, 7, 8]]
                ]
            }
        ],
        [Member(i) for i in [1, 2, 11, 4, 12, 3, 7, 5, 8, 10, 9, 6]],
        3,
        [
            # Nothing specific to validate
        ]
    ),
    # Silly example that failued due to bad role logic
    (
        [
            # No history
        ],
        [
            # print([(m.id, [r.id for r in m.roles]) for m in matchees]) to get the below
            Member(i, [Role(r) for r in roles]) for (i, roles) in
            [
                (4, [1, 2, 3, 4, 5, 6, 7, 8]),
                (8, [1]),
                (9, [1, 2, 3, 4, 5]),
                (6, [1, 2, 3]),
                (11, [1, 2, 3]),
                (7, [1, 2, 3, 4, 5, 6, 7]),
                (1, [1, 2, 3, 4]),
                (5, [1, 2, 3, 4, 5]),
                (12, [1, 2, 3, 4]),
                (10, [1]),
                (13, [1, 2, 3, 4, 5, 6]),
                (2, [1, 2, 3, 4, 5, 6]),
                (3, [1, 2, 3, 4, 5, 6, 7])
            ]
        ],
        2,
        [
            # Nothing else
        ]
    ),
    # Another weird one pulled out of the stress test
    (
        [
            # print([(str(h["ts"]), [[f"Member({gm.id})" for gm in g] for g in h["groups"]]) for h in history_data])
            {"ts": datetime.strptime(ts, r"%Y-%m-%d %H:%M:%S.%f"), "groups": [
                [Member(m) for m in group] for group in groups]}
            for (ts, groups) in
            [
                (
                    '2024-07-07 20:25:56.313993',
                    [
                        [1, 2, 3, 4, 5],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
                        [1],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
                        [1, 2, 3, 4, 5, 6, 7, 8]
                    ]
                ),
                (
                    '2024-07-13 20:25:56.313993',
                    [
                        [1, 2],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
                            12, 13, 14, 15, 16, 17, 18],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
                        [1]
                    ]
                ),
                (
                    '2024-06-29 20:25:56.313993',
                    [
                        [1, 2, 3, 4, 5],
                        [1, 2, 3, 4, 5, 6, 7],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
                            13, 14, 15, 16, 17, 18, 19, 20],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
                            11, 12, 13, 14, 15, 16, 17],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
                            12, 13, 14, 15, 16, 17, 18, 19, 20]
                    ]
                ),
                (
                    '2024-06-25 20:25:56.313993',
                    [
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
                            12, 13, 14, 15, 16, 17, 18],
                        [1, 2],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
                            12, 13, 14, 15, 16, 17, 18, 19],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
                        [1, 2]
                    ]
                ),
                (
                    '2024-07-04 20:25:56.313993',
                    [
                        [1, 2, 3, 4, 5],
                        [1, 2, 3],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
                        [1, 2, 3, 4, 5, 6, 7]
                    ]
                ),
                (
                    '2024-07-16 20:25:56.313993',
                    [
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
                            13, 14, 15, 16, 17, 18, 19, 20],
                        [1, 2, 3, 4, 5, 6],
                        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
                            11, 12, 13, 14, 15, 16, 17, 18]
                    ]
                )
            ]
        ],
        [
            # print([(m.id, [r.id for r in m.roles]) for m in matchees]) to get the below
            Member(i, [Role(r) for r in roles]) for (i, roles) in
            [
                (10, [1, 2, 3]),
                (4, [1, 2, 3]),
                (5, [1, 2]),
                (13, [1, 2]),
                (3, [1, 2, 3, 4]),
                (14, [1]),
                (6, [1, 2, 3, 4]),
                (11, [1]),
                (9, [1]),
                (1, [1, 2, 3]),
                (16, [1, 2]),
                (15, [1, 2]),
                (2, [1, 2, 3]),
                (7, [1, 2, 3]),
                (12, [1, 2]),
                (8, [1, 2, 3, 4])
            ]
        ],
        5,
        [
            # Nothing
        ]
    )
], ids=['simple_history', 'fallback', 'example_1', 'example_2', 'example_3'])
def test_unique_regressions(history_data, matchees, per_group, checks):
    """Test a bunch of unqiue failures that happened in the past"""

    # Replay the history
    for d in history_data:
        state.State.log_groups(d["groups"], d["ts"])

    groups = members_to_groups_validate(matchees, per_group)

    # Run the custom validate functions
    for check in checks:
        assert check(groups)


def random_chunk(li, min_chunk, max_chunk, rand):
    """
    "Borrowed" from https://stackoverflow.com/questions/21439011/best-way-to-split-a-list-into-randomly-sized-chunks
    """
    it = iter(li)
    while True:
        nxt = list(itertools.islice(it, rand.randint(min_chunk, max_chunk)))
        if nxt:
            yield nxt
        else:
            break


# Generate a large set of "interesting" tests that replay a fake history onto random people
# Increase these numbers for some extreme programming
@pytest.mark.parametrize("per_group, num_members, num_history", (
    (per_group, num_members, num_history)
    # Most of the time groups are gonna be from 2 to 5
    for per_group in range(2, 5)
    # Going lower than 8 members doesn't give the bot much of a chance
    # And it will fail to not fall back sometimes
    # That's probably OK frankly
    for num_members in range(8, 32, 5)
    # Throw up to 7 histories at the algorithmn
    for num_history in range(0, 8)))
def test_stess_random_groups(per_group, num_members, num_history):
    """Run a randomised test based on the input"""

    # Seed the random based on the inputs paird with primes
    # Ensures the test has interesting fake data, but is stable
    rand = random.Random(per_group*3 + num_members*5 + num_history*7)

    # Start with a list of all possible members
    possible_members = [Member(i) for i in range(num_members*2)]
    for member in possible_members:
        # Give each member 3 random roles from 1-7
        member.roles = [Role(i) for i in rand.sample(range(1, 8), 3)]

    # For each history item match up groups and log those
    for i in range(num_history+1):

        # Grab the num of members and replay
        rand.shuffle(possible_members)
        members = copy.deepcopy(possible_members[:num_members])

        groups = members_to_groups_validate(members, per_group)
        state.State.log_groups(
            groups, datetime.now() - timedelta(days=num_history-i))


def test_auth_scopes():

    id = "1"
    assert not state.State.get_user_has_scope(id, state.AuthScope.MATCHER)

    id = "2"
    state.State.set_user_scope(id, state.AuthScope.MATCHER)
    assert state.State.get_user_has_scope(id, state.AuthScope.MATCHER)

    # Validate the state by constucting a new one
    _ = state._State(state.State._dict)