import json
import os
from unittest.mock import MagicMock, patch
from django.contrib.auth.models import User
from django.test import TestCase
from datetime import date, timedelta

from esi.openapi_clients import ESIClientProvider
from django.core.cache import cache
from django.core.management import call_command
from django.utils import timezone
from esi.tests import NoSocketsTestCase
from httpx import RequestError, HTTPStatusError
from esi.exceptions import ESIErrorLimitException, HTTPClientError, HTTPNotModified, ESIBucketLimitException, HTTPServerError
from esi.rate_limiting import ESIRateLimits
from esi import app_settings
from esi import __title__, __url__, __version__
import httpx

from . import _generate_token, _store_as_Token
from .. import openapi_clients as oc

SPEC_PATH = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), "test_openapi.json"
)
MODULE_PATH_PLUGINS = 'esi.aiopenapi3.plugins'


class TestClientFunctions(TestCase):
    def test_time_to_expiry_valid(self):
        expires = (
            timezone.now() + timedelta(seconds=120)
        ).strftime('%a, %d %b %Y %H:%M:%S %Z')
        ttl = oc._time_to_expiry(expires)

        # this shouldnt take more that 10 seconds
        self.assertGreater(ttl, 110)

    def test_time_to_expiry_invalid(self):
        # invalid format returns 0
        self.assertEqual(oc._time_to_expiry("not-a-date"), 0)

    def test_httpx_exceptions_valids(self):
        self.assertTrue(
            oc._httpx_exceptions(
                RequestError("Bad Request")
            )
        )

        response = MagicMock(status_code=502)
        exc = HTTPStatusError("msg", request=None, response=response)

        self.assertTrue(
            oc._httpx_exceptions(exc)
        )

        response.status_code = 400
        exc = HTTPStatusError("msg", request=None, response=response)

        self.assertFalse(
            oc._httpx_exceptions(exc)
        )

        self.assertFalse(
            oc._httpx_exceptions(
                ESIErrorLimitException(reset=10)
            )
        )

    def test_httpx_exceptions_invalid(self):
        self.assertFalse(
            oc._httpx_exceptions(
                "this is not an exception!"
            )
        )


class BuildUserAgentTests(TestCase):
    app_name = "TestApp"
    app_ver = "1.2.3"
    app_url = "https://tests.pass"

    def test_build_user_agent_with_url(self):
        ua = oc._build_user_agent(self.app_name, self.app_ver, self.app_url)

        expected_app_name = "TestApp"
        expected_title = "DjangoEsi"

        self.assertEqual(
            (
                f"{expected_app_name}/{self.app_ver} "
                f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{self.app_url})'} "
                f"{expected_title}/{__version__} (+{__url__})"
            ),
            ua
        )

    def test_enforce_pascal_case_for_ua_appname_with_space(self):
        """
        Test that the application name is converted to PascalCase in the User-Agent string when it contains spaces.

        :return:
        :rtype:
        """

        ua = oc._build_user_agent("test app", self.app_ver, self.app_url)

        expected_app_name = "TestApp"
        expected_title = "DjangoEsi"

        self.assertEqual(
            (
                f"{expected_app_name}/{self.app_ver} "
                f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{self.app_url})'} "
                f"{expected_title}/{__version__} (+{__url__})"
            ),
            ua
        )

    def test_enforce_pascal_case_for_ua_appname_with_hyphen(self):
        """
        Test that the application name is converted to PascalCase in the User-Agent string when it contains hyphens.

        :return:
        :rtype:
        """

        ua = oc._build_user_agent("test-app", self.app_ver, self.app_url)

        expected_app_name = "TestApp"
        expected_title = "DjangoEsi"

        self.assertEqual(
            (
                f"{expected_app_name}/{self.app_ver} "
                f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{self.app_url})'} "
                f"{expected_title}/{__version__} (+{__url__})"
            ),
            ua
        )

    def test_build_user_agent_without_url(self):
        ua = oc._build_user_agent(self.app_name, self.app_ver)

        expected_app_name = "TestApp"
        expected_title = "DjangoEsi"

        self.assertEqual(
            (
                f"{expected_app_name}/{self.app_ver} "
                f"({app_settings.ESI_USER_CONTACT_EMAIL}) "
                f"{expected_title}/{__version__} (+{__url__})"
            ),
            ua
        )


class BaseEsiOperationTests(TestCase):
    def setUp(self):
        self.page_param = MagicMock()
        self.page_param.name = "page"

        self.after_param = MagicMock()
        self.after_param.name = "after"

        self.before_param = MagicMock()
        self.before_param.name = "before"

        self.data_param = MagicMock()
        self.data_param.name = "data"

        self.lang_param = MagicMock()
        self.lang_param.name = "Accept-Language"

        self.body_param = MagicMock()
        self.body_param.name = "body"

        self.fake_op = MagicMock()
        self.fake_op.parameters = [
            self.data_param,
            self.lang_param
        ]
        self.fake_op.tags = ["test"]
        self.fake_op.operationId = "fake_op"
        self.fake_op.extensions = {}
        self.api = MagicMock(app_name="TestApp")
        self.op = oc.BaseEsiOperation(
            ("GET", "/fake_op", self.fake_op, {}),
            self.api
        )

    def test_non_unique_kwargs(self):
        op_1 = self.op(data="bar")
        key_1 = op_1._cache_key()
        op_2 = self.op(data="foo")
        key_2 = op_2._cache_key()
        self.assertNotEqual(key_1, key_2)

    def test_unique_kwargs(self):
        op_1 = self.op(data="foo")
        key_1 = op_1._cache_key()
        op_2 = self.op(data="foo")
        key_2 = op_2._cache_key()
        self.assertEqual(key_1, key_2)

    def test_extract_body(self):
        test_body = "something somethng something..."
        op = self.op(body=test_body)
        body = op._extract_body_param()
        self.assertEqual(test_body, body)

    def test_extract_body_exception(self):
        test_body = "something somethng something..."
        self.fake_op.requestBody = False
        op = self.op(body=test_body)
        with self.assertRaises(ValueError):
            op._extract_body_param()

    def test_extract_token(self):
        test_tkn = {"token": "token model goes here"}
        op = self.op(token=test_tkn)
        token = op._extract_token_param()
        self.assertEqual(test_tkn, token)

    def test_extract_token_exception_no_token_needed(self):
        self.op._kwargs = {"token": "token"}
        self.fake_op.security = None
        with self.assertRaises(ValueError):
            self.op._extract_token_param()

    def test_not_page_or_cursor_param(self):
        self.assertFalse(self.op._has_page_param())
        self.assertFalse(self.op._has_cursor_param())

    def test_has_page_param(self):
        self.fake_op.parameters += [self.page_param]
        op = self.op()
        self.assertTrue(op._has_page_param())

    def test_has_cursor_params(self):
        self.fake_op.parameters = [self.after_param]
        op = self.op()
        self.assertTrue(op._has_cursor_param())

        self.fake_op.parameters = [self.before_param]
        op = self.op()
        self.assertTrue(op._has_cursor_param())


class EsiOperationTests(TestCase):
    def setUp(self):
        self.op_mock = MagicMock()
        self.op_mock.parameters = []
        self.op_mock.tags = ["tag"]
        self.op_mock.operationId = "opid"
        self.op_mock.extensions = {}

        self.op_mock_rate = MagicMock()
        self.op_mock_rate.parameters = []
        self.op_mock_rate.tags = ["tag"]
        self.op_mock_rate.operationId = "opidRated"
        self.op_mock_rate.extensions = {
            "rate-limit": {
                "group": "test-group",
                "max-tokens": 100,
                "window-size": "5m"
            }
        }

        self.api_mock = MagicMock()
        self.api_mock.app_name = "TestApp"

        self.op = oc.EsiOperation(
            (
                "GET",
                "/url",
                self.op_mock,
                {}
            ),
            self.api_mock
        )

        self.op_rate_rated = oc.EsiOperation(
            (
                "GET",
                "/url",
                self.op_mock_rate,
                {}
            ),
            self.api_mock
        )

    @patch.object(oc.EsiOperation, "_make_request")
    def test_result_and_results(self, mock_make_request):
        data = {"data": "stuff"}
        mock_resp = MagicMock(status_code=200, headers={"Expires": "Wed, 1 July 2099 11:00:00 GMT"})
        mock_make_request.return_value = ({"Expires": "date"}, data, mock_resp)
        data_resp = self.op(foo="bar").result()
        self.assertEqual(data, data_resp)

    def test_esi_bucket_public(self):
        op = self.op_rate_rated(foo="bar")
        self.assertEqual(op.bucket.window, 300)
        self.assertEqual(op.bucket.slug, "test-group")
        self.assertEqual(op.bucket.limit, 100)

    def test_esi_bucket_limit(self):
        op = self.op_rate_rated(foo="bar")
        ESIRateLimits.set_bucket(op.bucket, 0)

        with self.assertRaises(ESIBucketLimitException):
            op.result()


class TestOpenapiClientProvider(NoSocketsTestCase):
    def setUp(self):
        self.app_name = "TestsApp"
        self.app_ver = "1.2.3"
        self.app_url = "https://tests.pass"
        self.esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Status"],
            spec_file=SPEC_PATH
        )
        cache.clear()

    def test_str(self):
        self.assertIn(self.esi._ua_appname, str(self.esi))
        self.assertIn(self.esi._ua_version, str(self.esi))

    def test_compatibilitydate_date_to_string(self):
        testdate_1 = date(2024, 1, 1)
        testdate_2 = date(2025, 8, 26)

        self.assertEqual("2024-01-01", ESIClientProvider._date_to_string(testdate_1))
        self.assertEqual("2025-08-26", ESIClientProvider._date_to_string(testdate_2))

    def test_compatibility_date_as_date(self):
        testdate = date(2024, 1, 1)
        app_name = "TestsApp"
        app_ver = "1.2.3"
        app_url = "https://tests.pass"

        esi = ESIClientProvider(
            ua_appname=app_name,
            ua_url=app_url,
            ua_version=app_ver,
            compatibility_date=testdate,
            spec_file=SPEC_PATH
        )
        self.assertEqual(esi._compatibility_date, ESIClientProvider._date_to_string(testdate))


    @patch.object(httpx.Client, "send")
    def test_ua(self, send: MagicMock):
        send.return_value = httpx.Response(
            200,
            json={
                "players": 1234,
                "server_version": "1234",
                "start_time": "2029-09-19T11:02:08Z"
            },
            request=httpx.Request("GET", "test"),
        )

        status = self.esi.client.Status.GetStatus().result()
        call_args, call_kwargs = send.call_args

        expected_app_name = "TestsApp"
        expected_title = 'DjangoEsi'

        self.assertEqual(
            call_args[0].headers["user-agent"],
            (
                f"{expected_app_name}/{self.app_ver} "
                f"({app_settings.ESI_USER_CONTACT_EMAIL}{f'; +{self.app_url})'} "
                f"{expected_title}/{__version__} (+{__url__})"
            )
        )
        self.assertEqual(status.players, 1234)

    @patch(MODULE_PATH_PLUGINS + '.settings.DEBUG', True)
    def test_no_tag_no_op_debug(self):
        app_name = "TestsApp"
        app_ver = "1.2.3"
        app_url = "https://tests.pass"

        esi = ESIClientProvider(
            ua_appname=app_name,
            ua_url=app_url,
            ua_version=app_ver,
            compatibility_date="2020-01-01",
            spec_file=SPEC_PATH
        )
        self.assertIsNotNone(esi.client.Status)
        self.assertIsNotNone(esi.client.Status.GetStatus)

    @patch(MODULE_PATH_PLUGINS + '.settings.DEBUG', True)
    def test_tag_no_op_debug(self):
        app_name = "TestsApp"
        app_ver = "1.2.3"
        app_url = "https://tests.pass"

        esi = ESIClientProvider(
            ua_appname=app_name,
            ua_url=app_url,
            ua_version=app_ver,
            compatibility_date="2020-01-01",
            tags=["Status"],
            spec_file=SPEC_PATH
        )
        self.assertIsNotNone(esi.client.Status)
        self.assertIsNotNone(esi.client.Status.GetStatus)

    @patch(MODULE_PATH_PLUGINS + '.settings.DEBUG', True)
    def test_no_tag_op_debug(self):
        app_name = "TestsApp"
        app_ver = "1.2.3"
        app_url = "https://tests.pass"

        esi = ESIClientProvider(
            ua_appname=app_name,
            ua_url=app_url,
            ua_version=app_ver,
            compatibility_date="2020-01-01",
            operations=["GetStatus"],
            spec_file=SPEC_PATH
        )
        self.assertIsNotNone(esi.client.Status)
        self.assertIsNotNone(esi.client.Status.GetStatus)

    @patch(MODULE_PATH_PLUGINS + '.settings.DEBUG', False)
    def test_no_tag_no_op_no_debug(self):
        app_name = "TestsApp"
        app_ver = "1.2.3"
        app_url = "https://tests.pass"

        with self.assertRaises(AttributeError):
            esi = ESIClientProvider(
                ua_appname=app_name,
                ua_url=app_url,
                ua_version=app_ver,
                compatibility_date="2020-01-01",
                spec_file=SPEC_PATH
            )
            esi.client

    @patch.object(httpx.Client, "send")
    def test_no_bucket(self, send: MagicMock):
        self.esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Universe"],
            spec_file=SPEC_PATH
        )

        send.return_value = httpx.Response(
            200,
            json=[1, 2, 3, 4],
            request=httpx.Request("GET", "test"),
        )

        types = self.esi.client.Universe.GetUniverseTypes().result()
        self.assertEqual(len(types), 4)

    @patch.object(httpx.Client, "send")
    def test_etag_hit_cached(self, send: MagicMock):
        etag = "'123456789abcdef123456789abcdef'"

        expires = (
            timezone.now() + timedelta(minutes=5)
        ).strftime('%a, %d %b %Y %H:%M:%S %Z')

        send.return_value = httpx.Response(
            200,
            json={
                "players": 1234,
                "server_version": "1234",
                "start_time": "2029-09-19T11:02:08Z"
            },
            headers={
                "etag": etag,
                "expires": expires
            },
            request=httpx.Request(
                "GET",
                "test",
            ),
        )

        self.esi.client.Status.GetStatus().result()

        with self.assertRaises(HTTPNotModified):
            self.esi.client.Status.GetStatus().result()

    @patch.object(httpx.Client, "send")
    def test_pages_etag_partial_hit_cached(self, send: MagicMock):
        self.esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Assets"],
            spec_file=SPEC_PATH
        )

        etag = "'123456789abcdef123456789abcdef'"

        expires = (
            timezone.now() + timedelta(minutes=5)
        ).strftime('%a, %d %b %Y %H:%M:%S %Z')

        send.return_value = httpx.Response(
            200,
            json=[
                {
                    "is_blueprint_copy": None,
                    "is_singleton": True,
                    "item_id": 1234,
                    "location_flag": "HiSlot6",
                    "location_id": 123456,
                    "location_type": "item",
                    "quantity": 1,
                    "type_id": 123456
                }
            ],
            headers={
                "etag": etag,
                "expires": expires,
                "x-pages": "5"
            },
            request=httpx.Request(
                "GET",
                "test",
            ),
        )

        # get one page only to have a single etag filled in
        hit_1 = self.esi.client.Assets.GetCharactersCharacterIdAssets(
            character_id=123,
            token="FakeToken",
            page=1
        ).result()

        # now i want all of them and give them to me
        assets = self.esi.client.Assets.GetCharactersCharacterIdAssets(
            character_id=123,
            token="FakeToken"
        ).results()

        print(vars(send))

        self.assertEqual(len(assets), 5)
        self.assertEqual(send.call_count, 5) # only hit esi for 5 pages, use cache for rest.

        with self.assertRaises(HTTPNotModified):
            # now we have all the data and throw this error
            assets = self.esi.client.Assets.GetCharactersCharacterIdAssets(
                character_id=123,
                token="FakeToken"
            ).results()

    @patch.object(httpx.Client, "send")
    def test_etag_not_hit_cached(self, send: MagicMock):
        etag = "'123456789abcdef123456789abcdef'"

        expires = (
            timezone.now() + timedelta(minutes=5)
        ).strftime('%a, %d %b %Y %H:%M:%S %Z')

        send.return_value = httpx.Response(
            200,
            json={
                "players": 1234,
                "server_version": "1234",
                "start_time": "2029-09-19T11:02:08Z"
            },
            headers={
                "etag": etag,
                "expires": expires
            },
            request=httpx.Request(
                "GET",
                "test",
            ),
        )

        self.esi.client.Status.GetStatus().result()

        result = self.esi.client.Status.GetStatus().result(use_etag=False)
        self.assertEqual(result.players, 1234)

    @patch.object(httpx.Client, "send")
    def test_force_refresh(self, send: MagicMock):
        etag = "'123456789abcdef123456789abcdef'"

        expires = (
            timezone.now() + timedelta(minutes=5)
        ).strftime('%a, %d %b %Y %H:%M:%S %Z')

        send.return_value = httpx.Response(
            200,
            json={
                "players": 1234,
                "server_version": "1234",
                "start_time": "2029-09-19T11:02:08Z"
            },
            headers={
                "etag": etag,
                "expires": expires
            },
            request=httpx.Request(
                "GET",
                "test",
            ),
        )

        self.esi.client.Status.GetStatus().result()

        result = self.esi.client.Status.GetStatus().result(force_refresh=True)
        self.assertEqual(result.players, 1234)
        self.assertEqual(send.call_count, 2)

    @patch.object(httpx.Client, "send")
    def test_404(self, send: MagicMock):
        self.esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Universe"],
            spec_file=SPEC_PATH
        )

        send.return_value = httpx.Response(
            404,
            json={
                "error": "error"
            },
            headers={
                "X-RateLimit-Reset": "15",
                "X-RateLimit-Remaining": "0"
            },
            request=httpx.Request(
                "GET",
                "/universe/types"
            ),
        )

        with self.assertRaises(HTTPClientError):
            self.esi.client.Universe.GetUniverseTypes().result()

    @patch.object(httpx.Client, "send")
    def test_420(self, send: MagicMock):
        self.esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Universe"],
            spec_file=SPEC_PATH
        )

        send.return_value = httpx.Response(
            420,
            json={
                "error": "error"
            },
            headers={
                "X-RateLimit-Reset": "15",
                "X-RateLimit-Remaining": "0"
            },
            request=httpx.Request(
                "GET",
                "/universe/types"
            ),
        )

        with self.assertRaises(ESIErrorLimitException):
            self.esi.client.Universe.GetUniverseTypes().result()

        self.assertGreater(cache.get("esi_error_limit_reset"), 10)

    @patch.object(httpx.Client, "send")
    def test_420_past(self, send: MagicMock):
        self.esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Universe"],
            spec_file=SPEC_PATH
        )

        send.return_value = httpx.Response(
            420,
            json={
                "error": "error"
            },
            headers={
                "X-RateLimit-Remaining": "0"
            },
            request=httpx.Request(
                "GET",
                "/universe/types"
            ),
        )

        with self.assertRaises(ESIErrorLimitException):
            self.esi.client.Universe.GetUniverseTypes().result()

        self.assertIsNone(cache.get("esi_error_limit_reset"))

    @patch.object(httpx.Client, "send")
    def test_rate_bucket(self, send: MagicMock):
        send.return_value = httpx.Response(
            200,
            json={
                "players": 1234,
                "server_version": "1234",
                "start_time": "2029-09-19T11:02:08Z"
            },
            headers={
                "x-ratelimit-group": "status",
                "x-ratelimit-used": "2",
                "x-ratelimit-remaining": "598",
                "x-ratelimit-limit": "600/15m",
            },
            request=httpx.Request(
                "GET",
                "/status"
            ),
        )
        self.esi.client.Status.GetStatus().result()
        self.assertEqual(
            ESIRateLimits.get_bucket(self.esi.client.Status.GetStatus().bucket),
            598
        )

    @patch.object(httpx.Client, "send")
    def test_server_error(self, send: MagicMock):
        send.return_value = httpx.Response(
            520,
            json={
                "error": "error"
            },
            headers={
                "x-ratelimit-group": "status",
                "x-ratelimit-used": "5",
                "x-ratelimit-remaining": "595",
                "x-ratelimit-limit": "600/15m",
            },
            request=httpx.Request(
                "GET",
                "/status"
            ),
        )
        with self.assertRaises(HTTPServerError):
            self.esi.client.Status.GetStatus().result()

    def test_minified_op_not_found(self):
        with self.assertRaises(AttributeError):
            self.esi.client.Universe.GetUniverseTypes()

    def test_minified_op_not_found(self):
        self.esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Universe"],
            spec_file=SPEC_PATH
        )

        with self.assertRaises(AttributeError):
            self.esi.client.Universe.GetUniverseAncestries()

    def test_rate_bucket_found_in_spec(self):

        op = self.esi.client.Status.GetStatus()

        self.assertIsNotNone(op.bucket)
        self.assertEqual(op.bucket.limit, 600)
        self.assertEqual(op.bucket.slug, "status")
        self.assertEqual(op.bucket.window, 900)

    def test_rate_bucket_hit(self):
        op = self.esi.client.Status.GetStatus()

        ESIRateLimits.set_bucket(op.bucket, 0)

        with self.assertRaises(ESIBucketLimitException):
            op.result()

    def test_global_limit_hit(self):
        op = self.esi.client.Status.GetStatus()

        cache.set("esi_error_limit_reset", 15, 15)

        with self.assertRaises(ESIErrorLimitException):
            op.result()

    @patch.object(httpx.Client, "send")
    def test_load_sync(self, send: MagicMock):
        esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Status"]
        )
        cache.clear()
        expires = (
            timezone.now() + timedelta(minutes=5)
        ).strftime('%a, %d %b %Y %H:%M:%S %Z')

        spec = None
        with open(SPEC_PATH) as f:
            spec = json.load(f)

        send.return_value = httpx.Response(
            200,
            json=spec,
            headers={
                "expires": expires
            },
            request=httpx.Request(
                "GET",
                "test",
            ),
        )

        esi.client
        self.assertIsNotNone(esi.client.Status)


class TestTokenisedEndpoints(TestCase):
    def setUp(self):
        self.app_name = "TestsApp"
        self.app_ver = "1.2.3"
        self.app_url = "https://tests.pass"
        self.esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Character", "Assets"],
            spec_file=SPEC_PATH
        )
        cache.clear()

        self.character_id = 1000
        character_name = 'Bruce Wayne'

        self.user = User.objects.create_user(
            character_name,
            'abc@example.com',
            'password'
        )

        self.token_str = _store_as_Token(
            _generate_token(
                character_id=self.character_id,
                character_name=character_name,
                scopes=['esi-assets.read_assets.v1']
            ),
            self.user
        )

        self.token_note = _store_as_Token(
            _generate_token(
                character_id=self.character_id,
                character_name=character_name,
                scopes=['esi-characters.read_notifications.v1']
            ),
            self.user
        )

        self.resp_note = httpx.Response(
            200,
            json=[{
                "is_read": None,
                "notification_id": 123456,
                "sender_id": 12345,
                "sender_type": "corporation",
                "text": "text...",
                "timestamp": "2024-11-25T05:37:00Z",
                "type": "CorpAllBillMsg"
            },
                {
                "is_read": None,
                "notification_id": 123457,
                "sender_id": 123456,
                "sender_type": "corporation",
                "text": "text...",
                "timestamp": "2024-11-25T01:43:00Z",
                "type": "MoonminingExtractionStarted"
            },
            ],
            headers={
                "x-ratelimit-group": "char-notification",
                "x-ratelimit-used": "5",
                "x-ratelimit-remaining": "595",
                "x-ratelimit-limit": "600/15m",
            },
            request=httpx.Request(
                "GET",
                "/characters/{character_id}/notifications"
            ),
        )

        self.resp_asset = httpx.Response(
            200,
            json=[
                {
                    "is_blueprint_copy": None,
                    "is_singleton": False,
                    "item_id": 12346,
                    "location_flag": "Hangar",
                    "location_id": 60003760,
                    "location_type": "station",
                    "quantity": 999,
                    "type_id": 3764
                },
                {
                    "is_blueprint_copy": None,
                    "is_singleton": False,
                    "item_id": 12345,
                    "location_flag": "Hangar",
                    "location_id": 60003760 ,
                    "location_type": "station",
                    "quantity": 999,
                    "type_id": 11567
                }
            ],
            headers={
                "x-esi-error-limit-remain": "100",
                "x-esi-error-limit-reset": "60",
                "x-pages": "5"
            },
            request=httpx.Request(
                "GET",
                "/characters/{character_id}/notifications"
            ),
        )

    @patch.object(httpx.Client, "send")
    def test_no_token_provided(self, send: MagicMock):
        send.return_value = self.resp_note
        with self.assertRaises(ValueError):
            self.esi.client.Character.GetCharactersCharacterIdNotifications(
                character_id=self.character_id
            ).result()
        # Didn't hit ESI
        self.assertEqual(send.call_count, 0)

    @patch.object(httpx.Client, "send")
    def test_good_token_provided(self, send: MagicMock):
        send.return_value = self.resp_note
        results = self.esi.client.Character.GetCharactersCharacterIdNotifications(
            character_id=self.character_id,
            token=self.token_note
        ).result()
        self.assertEqual(send.call_count, 1)
        self.assertEqual(len(results), 2)

    @patch.object(httpx.Client, "send")
    def test_bad_token_provided(self, send: MagicMock):
        send.return_value = self.resp_note
        with self.assertRaises(ValueError):
            self.esi.client.Character.GetCharactersCharacterIdNotifications(
                character_id=self.character_id,
                token=self.token_str
            ).result()
        # Didn't hit ESI
        self.assertEqual(send.call_count, 0)

    @patch.object(httpx.Client, "send")
    def test_token_pages(self, send: MagicMock):
        send.return_value = self.resp_asset
        results = self.esi.client.Assets.GetCharactersCharacterIdAssets(
            character_id=self.character_id,
            token=self.token_str
        ).results()
        self.assertEqual(send.call_count, 5)
        self.assertEqual(len(results), 10)

class TestTokenisedEndpoints(TestCase):
    def setUp(self):
        cache.clear()
        self.app_name = "TestsApp"
        self.app_ver = "1.2.3"
        self.app_url = "https://tests.pass"
        spec = {}
        with open(SPEC_PATH) as f:
            spec = json.load(f)

        self.resp = httpx.Response(
            200,
            json=spec,
            headers={
                "x-esi-error-limit-remain": "100",
                "x-esi-error-limit-reset": "60"
            },
            request=httpx.Request(
                "GET",
                "https://esi.evetech.net/meta/openapi.json"
            ),
        )

    @patch.object(httpx.Client, "get")
    def test_load_spec(self, get: MagicMock):
        get.return_value = self.resp

        esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Character", "Assets"],
        )
        esi.client
        self.assertEqual(get.call_count, 1)

        esi2 = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-10-10",
            tags=["status"],
        )
        esi2.client
        self.assertEqual(get.call_count, 2)

        esi3 = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Character"],
        )
        esi3.client
        self.assertEqual(get.call_count, 2)

        esi4 = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-10-10",
            tags=["Assets"],
        )
        esi4.client
        self.assertEqual(get.call_count, 2)

    @patch.object(httpx.Client, "get")
    def test_purge_cache_load_spec(self, get: MagicMock):
        get.return_value = self.resp

        esi = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Character", "Assets"],
        )
        esi.client
        self.assertEqual(get.call_count, 1)

        call_command("esi_clear_spec_cache")

        esi2 = ESIClientProvider(
            ua_appname=self.app_name,
            ua_url=self.app_url,
            ua_version=self.app_ver,
            compatibility_date="2020-01-01",
            tags=["Character", "Assets"],
        )
        esi2.client
        self.assertEqual(get.call_count, 2)
