import unittest
from collections import OrderedDict

import pytest

from moto.core.exceptions import InvalidToken
from moto.utilities.paginator import Paginator, paginate

results = [
    {"id": f"id{i}", "name": f"name{i}", "arn": f"arn:aws:thing/name{i}"}
    for i in range(0, 10)
]


class Model:
    def __init__(self, i):
        self.id = f"id{i}"
        self.name = f"name{i}"
        self.arn = f"arn:aws:thing/{self.name}"


model_results = [Model(i) for i in range(0, 100)]


def test_paginator_without_max_results__throws_error():
    p = Paginator()
    with pytest.raises(TypeError):
        p.paginate(results)


def test_paginator__paginate_with_just_max_results():
    p = Paginator(max_results=50)
    resp = p.paginate(results)
    assert len(resp) == 2

    page, next_token = resp
    assert next_token is None
    assert page == results


def test_paginator__ordered_dict():
    p = Paginator(max_results=1, unique_attribute="id")
    page, _ = p.paginate([OrderedDict(x) for x in results])
    assert len(page) == 1


def test_paginator__paginate_without_range_key__throws_error():
    p = Paginator(max_results=2)
    with pytest.raises(KeyError):
        p.paginate(results)


def test_paginator__paginate_with_unknown_range_key__throws_error():
    p = Paginator(max_results=2, unique_attribute=["unknown"])
    with pytest.raises(KeyError):
        p.paginate(results)


def test_paginator__paginate_5():
    p = Paginator(max_results=5, unique_attribute=["name"])
    resp = p.paginate(results)
    assert len(resp) == 2

    page, next_token = resp
    assert next_token is not None
    assert page == results[0:5]


def test_paginator__paginate_5__use_different_range_keys():
    p = Paginator(max_results=5, unique_attribute="name")
    _, token_as_str = p.paginate(results)

    p = Paginator(max_results=5, unique_attribute=["name"])
    _, token_as_lst = p.paginate(results)

    assert token_as_lst is not None
    assert token_as_lst == token_as_str

    p = Paginator(max_results=5, unique_attribute=["name", "arn"])
    _, token_multiple = p.paginate(results)
    assert token_multiple is not None
    assert token_multiple != token_as_str


def test_paginator__paginate_twice():
    p = Paginator(max_results=5, unique_attribute=["name"])
    resp = p.paginate(results)
    assert len(resp) == 2

    page, next_token = resp

    p = Paginator(max_results=10, unique_attribute=["name"], starting_token=next_token)
    resp = p.paginate(results)

    page, next_token = resp
    assert next_token is None
    assert page == results[5:]


def test_paginator__invalid_token():
    with pytest.raises(InvalidToken):
        Paginator(max_results=5, unique_attribute=["name"], starting_token="unknown")


def test_paginator__invalid_token__but_we_just_dont_care():
    p = Paginator(
        max_results=5,
        unique_attribute=["name"],
        starting_token="unknown",
        fail_on_invalid_token=False,
    )
    res, token = p.paginate(results)

    assert res == []
    assert token is None


class CustomInvalidTokenException(BaseException):
    def __init__(self, token):
        self.message = f"Invalid token: {token}"


class GenericInvalidTokenException(BaseException):
    def __init__(self):
        self.message = "Invalid token!"


class TestDecorator(unittest.TestCase):
    PAGINATION_MODEL = {
        "method_returning_dict": {
            "input_token": "next_token",
            "limit_key": "max_results",
            "limit_default": 100,
            "unique_attribute": "name",
        },
        "method_returning_instances": {
            "input_token": "next_token",
            "limit_key": "max_results",
            "limit_default": 10,
            "limit_max": 50,
            "unique_attribute": "name",
        },
        "method_returning_args": {
            "limit_key": "max_results",
            "unique_attribute": "name",
        },
        "method_specifying_invalidtoken_exception": {
            "limit_key": "max_results",
            "limit_default": 5,
            "unique_attribute": "name",
            "fail_on_invalid_token": CustomInvalidTokenException,
        },
        "method_specifying_generic_invalidtoken_exception": {
            "limit_key": "max_results",
            "limit_default": 5,
            "unique_attribute": "name",
            "fail_on_invalid_token": GenericInvalidTokenException,
        },
        "method_expecting_token_as_kwarg": {
            "input_token": "custom_token",
            "limit_default": 1,
            "unique_attribute": "name",
        },
        "method_expecting_limit_as_kwarg": {
            "limit_key": "custom_limit",
            "limit_default": 1,
            "unique_attribute": "name",
        },
        "method_with_list_as_kwarg": {"limit_default": 1, "unique_attribute": "name"},
    }

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def method_returning_dict(self):
        return results

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def method_returning_instances(self):
        return model_results

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def method_without_configuration(self):
        return results

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def method_returning_args(self, *args, **kwargs):
        return [*args] + list(kwargs.items())

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def method_expecting_token_as_kwarg(self, custom_token=None):
        self.custom_token = custom_token
        return [{"name": "item1"}, {"name": "item2"}]

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def method_expecting_limit_as_kwarg(self, custom_limit):
        self.custom_limit = custom_limit
        return [{"name": "item1"}, {"name": "item2"}]

    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
    def method_with_list_as_kwarg(self, resources=None):
        if not resources:
            resources = []
        return resources or results

    @paginate(PAGINATION_MODEL)  # type: ignore[misc]
    def method_specifying_invalidtoken_exception(self):
        return results

    @paginate(PAGINATION_MODEL)  # type: ignore[misc]
    def method_specifying_generic_invalidtoken_exception(self):
        return results

    def test__method_returning_dict(self):
        page, token = self.method_returning_dict()
        assert page == results
        assert token is None

    def test__method_returning_instances(self):
        page, token = self.method_returning_instances()
        assert page == model_results[0:10]
        assert token is not None

    def test__method_without_configuration(self):
        with pytest.raises(ValueError):
            self.method_without_configuration()

    def test__input_arguments_are_returned(self):
        resp, _ = self.method_returning_args(1, "2", next_token=None, max_results=5)
        assert len(resp) == 4
        assert 1 in resp
        assert "2" in resp
        assert ("next_token", None) in resp
        assert ("max_results", 5) in resp

    def test__pass_exception_on_invalid_token(self):
        # works fine if no token is specified
        self.method_specifying_invalidtoken_exception()

        # throws exception if next_token is invalid
        with pytest.raises(CustomInvalidTokenException) as exc:
            self.method_specifying_invalidtoken_exception(
                next_token="some invalid token"
            )
        assert isinstance(exc.value, CustomInvalidTokenException)
        assert exc.value.message == "Invalid token: some invalid token"

    def test__pass_generic_exception_on_invalid_token(self):
        # works fine if no token is specified
        self.method_specifying_generic_invalidtoken_exception()

        # throws exception if next_token is invalid
        # Exception does not take any arguments - our paginator needs to
        # verify whether the next_token arg is expected
        with pytest.raises(GenericInvalidTokenException) as exc:
            self.method_specifying_generic_invalidtoken_exception(
                next_token="some invalid token"
            )
        assert isinstance(exc.value, GenericInvalidTokenException)
        assert exc.value.message == "Invalid token!"

    def test__invoke_function_that_expects_token_as_keyword(self):
        resp, first_token = self.method_expecting_token_as_kwarg()
        assert resp == [{"name": "item1"}]
        assert first_token is not None
        assert self.custom_token is None

        # Verify the custom_token is received in the business method
        # Could be handy for additional validation
        resp, _ = self.method_expecting_token_as_kwarg(custom_token=first_token)
        assert self.custom_token == first_token

    def test__invoke_function_that_expects_limit_as_keyword(self):
        self.method_expecting_limit_as_kwarg(custom_limit=None)
        assert self.custom_limit is None

        # Verify the custom_limit is received in the business method
        # Could be handy for additional validation
        self.method_expecting_limit_as_kwarg(custom_limit=1)
        assert self.custom_limit == 1

    def test__verify_kwargs_can_be_a_list(self):
        # Use case - verify that the kwarg can be of type list
        # Paginator creates a hash for all kwargs
        # We need to be make sure that the hash-function can deal with lists
        resp, token = self.method_with_list_as_kwarg()
        assert resp == results[0:1]

        resp, token = self.method_with_list_as_kwarg(next_token=token)
        assert resp == results[1:2]

        custom_list = [{"name": "a"}, {"name": "b"}]
        resp, token = self.method_with_list_as_kwarg(resources=custom_list)
        assert resp == custom_list[0:1]

        resp, token = self.method_with_list_as_kwarg(
            resources=custom_list, next_token=token
        )
        assert resp == custom_list[1:]
        assert token is None

    def test__paginator_fails_with_inconsistent_arguments(self):
        custom_list = [{"name": "a"}, {"name": "b"}]
        resp, token = self.method_with_list_as_kwarg(resources=custom_list)
        assert resp == custom_list[0:1]

        with pytest.raises(InvalidToken):
            # This should fail, as our 'resources' argument is inconsistent
            # with the original resources that were provided
            self.method_with_list_as_kwarg(resources=results, next_token=token)
