import datetime as dt
from unittest import skipIf
from unittest.mock import patch

from celery.exceptions import Retry as CeleryRetry

from django.test import override_settings
from django.utils.timezone import now
from esi.models import Token
from eveuniverse.models import EveSolarSystem, EveType
from eveuniverse.tests.testdata.factories import create_eve_entity

from allianceauth.eveonline.models import EveCharacter
from app_utils.esi import EsiErrorLimitExceeded, EsiOffline, EsiStatus
from app_utils.esi_testing import build_http_error
from app_utils.testing import (
    NoSocketsTestCase,
    create_authgroup,
    create_user_from_evecharacter,
    generate_invalid_pk,
)

from memberaudit import tasks
from memberaudit.models import (
    Character,
    CharacterAsset,
    CharacterUpdateStatus,
    Location,
)

from .testdata.esi_client_stub import esi_client_stub, esi_error_stub, esi_stub
from .testdata.factories import create_character, create_compliance_group_designation
from .testdata.load_entities import load_entities
from .testdata.load_eveuniverse import load_eveuniverse
from .testdata.load_locations import load_locations
from .utils import TOX_IS_RUNNING, create_memberaudit_character

MODELS_PATH = "memberaudit.models"
MANAGERS_PATH = "memberaudit.managers"
TASKS_PATH = "memberaudit.tasks"


class TestCaseTasks(NoSocketsTestCase):
    fixtures = ["disable_analytics.json"]


@patch(TASKS_PATH + ".update_compliance_groups_for_all", spec=True)
@patch(TASKS_PATH + ".update_all_characters", spec=True)
@patch(TASKS_PATH + ".update_market_prices", spec=True)
class TestRegularUpdates(TestCaseTasks):
    def test_should_run_update_for_all_except_compliance_groups(
        self,
        mock_update_market_prices,
        mock_update_all_characters,
        mock_update_compliance_groups_for_all,
    ):
        # when
        tasks.run_regular_updates()
        # then
        self.assertTrue(mock_update_market_prices.apply_async.called)
        self.assertTrue(mock_update_all_characters.apply_async.called)
        self.assertFalse(mock_update_compliance_groups_for_all.apply_async.called)

    def test_should_run_update_for_all_incl_compliance_groups(
        self,
        mock_update_market_prices,
        mock_update_all_characters,
        mock_update_compliance_groups_for_all,
    ):
        # given
        group = create_authgroup(internal=False)
        create_compliance_group_designation(group)
        # when
        tasks.run_regular_updates()
        # then
        self.assertTrue(mock_update_market_prices.apply_async.called)
        self.assertTrue(mock_update_all_characters.apply_async.called)
        self.assertTrue(mock_update_compliance_groups_for_all.apply_async.called)


class TestOtherTasks(TestCaseTasks):
    @patch(TASKS_PATH + ".EveMarketPrice.objects.update_from_esi", spec=True)
    def test_update_market_prices(self, mock_update_from_esi):
        tasks.update_market_prices()
        self.assertTrue(mock_update_from_esi.called)


@override_settings(CELERY_ALWAYS_EAGER=True)  # need to ignore exceptions
@patch(MANAGERS_PATH + ".character_sections_1.esi")
class TestUpdateCharacterAssets(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        load_entities()
        load_locations()
        cls.character_1001 = create_memberaudit_character(1001)
        cls.character_1002 = create_memberaudit_character(1002)
        cls.token = (
            cls.character_1001.eve_character.character_ownership.user.token_set.first()
        )
        cls.jita = EveSolarSystem.objects.get(id=30000142)
        cls.jita_44 = Location.objects.get(id=60003760)
        cls.amamake = EveSolarSystem.objects.get(id=30002537)
        cls.structure_1 = Location.objects.get(id=1000000000001)

    def test_update_assets_1(self, mock_esi):
        """can create assets from scratch"""
        mock_esi.client = esi_client_stub

        tasks.update_character_assets(self.character_1001.pk)
        self.assertSetEqual(
            set(self.character_1001.assets.values_list("item_id", flat=True)),
            {
                1100000000001,
                1100000000002,
                1100000000003,
                1100000000004,
                1100000000005,
                1100000000006,
                1100000000007,
                1100000000008,
            },
        )

        asset = self.character_1001.assets.get(item_id=1100000000001)
        self.assertTrue(asset.is_blueprint_copy)
        self.assertTrue(asset.is_singleton)
        self.assertEqual(asset.location_flag, "Hangar")
        self.assertEqual(asset.location_id, 60003760)
        self.assertEqual(asset.quantity, 1)
        self.assertEqual(asset.eve_type, EveType.objects.get(id=20185))
        self.assertEqual(asset.name, "Parent Item 1")

        asset = self.character_1001.assets.get(item_id=1100000000002)
        self.assertFalse(asset.is_blueprint_copy)
        self.assertTrue(asset.is_singleton)
        self.assertEqual(asset.location_flag, "???")
        self.assertEqual(asset.parent.item_id, 1100000000001)
        self.assertEqual(asset.quantity, 1)
        self.assertEqual(asset.eve_type, EveType.objects.get(id=19540))
        self.assertEqual(asset.name, "Leaf Item 2")

        asset = self.character_1001.assets.get(item_id=1100000000003)
        self.assertEqual(asset.parent.item_id, 1100000000001)
        self.assertEqual(asset.eve_type, EveType.objects.get(id=23))

        asset = self.character_1001.assets.get(item_id=1100000000004)
        self.assertEqual(asset.parent.item_id, 1100000000003)
        self.assertEqual(asset.eve_type, EveType.objects.get(id=19553))

        asset = self.character_1001.assets.get(item_id=1100000000005)
        self.assertEqual(asset.location, self.structure_1)
        self.assertEqual(asset.eve_type, EveType.objects.get(id=20185))

        asset = self.character_1001.assets.get(item_id=1100000000006)
        self.assertEqual(asset.parent.item_id, 1100000000005)
        self.assertEqual(asset.eve_type, EveType.objects.get(id=19540))

        asset = self.character_1001.assets.get(item_id=1100000000007)
        self.assertEqual(asset.location_id, 30000142)
        self.assertEqual(asset.name, "")
        self.assertEqual(asset.eve_type, EveType.objects.get(id=19540))

        asset = self.character_1001.assets.get(item_id=1100000000008)
        self.assertEqual(asset.location_id, 1000000000001)

    def test_update_assets_2(self, mock_esi):
        """can remove obsolete assets"""
        mock_esi.client = esi_client_stub
        CharacterAsset.objects.create(
            character=self.character_1001,
            item_id=1100000000666,
            location=self.jita_44,
            eve_type=EveType.objects.get(id=20185),
            is_singleton=False,
            name="Trucker",
            quantity=1,
        )

        tasks.update_character_assets(self.character_1001.pk)
        self.assertSetEqual(
            set(self.character_1001.assets.values_list("item_id", flat=True)),
            {
                1100000000001,
                1100000000002,
                1100000000003,
                1100000000004,
                1100000000005,
                1100000000006,
                1100000000007,
                1100000000008,
            },
        )

    def test_update_assets_3(self, mock_esi):
        """can update existing assets"""
        mock_esi.client = esi_client_stub
        CharacterAsset.objects.create(
            character=self.character_1001,
            item_id=1100000000001,
            location=self.jita_44,
            eve_type=EveType.objects.get(id=20185),
            is_singleton=True,
            name="Parent Item 1",
            quantity=10,
        )

        tasks.update_character_assets(self.character_1001.pk)
        self.assertSetEqual(
            set(self.character_1001.assets.values_list("item_id", flat=True)),
            {
                1100000000001,
                1100000000002,
                1100000000003,
                1100000000004,
                1100000000005,
                1100000000006,
                1100000000007,
                1100000000008,
            },
        )

        asset = self.character_1001.assets.get(item_id=1100000000001)
        self.assertTrue(asset.is_singleton)
        self.assertEqual(asset.location_id, 60003760)
        self.assertEqual(asset.quantity, 1)
        self.assertEqual(asset.eve_type, EveType.objects.get(id=20185))
        self.assertEqual(asset.name, "Parent Item 1")

    def test_update_assets_4(self, mock_esi):
        """assets moved to different locations are kept"""
        mock_esi.client = esi_client_stub
        parent_asset = CharacterAsset.objects.create(
            character=self.character_1001,
            item_id=1100000000666,
            location=self.jita_44,
            eve_type=EveType.objects.get(id=20185),
            is_singleton=True,
            name="Obsolete Container",
            quantity=1,
        )
        CharacterAsset.objects.create(
            character=self.character_1001,
            item_id=1100000000002,
            parent=parent_asset,
            eve_type=EveType.objects.get(id=19540),
            is_singleton=True,
            is_blueprint_copy=False,
            quantity=1,
        )

        tasks.update_character_assets(self.character_1001.pk)
        self.assertSetEqual(
            set(self.character_1001.assets.values_list("item_id", flat=True)),
            {
                1100000000001,
                1100000000002,
                1100000000003,
                1100000000004,
                1100000000005,
                1100000000006,
                1100000000007,
                1100000000008,
            },
        )

    def test_update_assets_5(self, mock_esi):
        """when update succeeded then report update success"""
        mock_esi.client = esi_client_stub

        tasks.update_character_assets(self.character_1001.pk)

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.ASSETS
        )
        self.assertTrue(status.is_success)
        self.assertFalse(status.last_error_message)

    def test_update_assets_6(self, mock_esi):
        """when update failed then report the error"""
        # given
        exception = build_http_error(502, "Test exception")
        mock_esi.client.Assets.get_characters_character_id_assets.side_effect = (
            exception
        )
        # when
        with self.assertRaises(OSError):
            tasks.update_character_assets(self.character_1001.pk)
        # then
        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.ASSETS
        )
        self.assertFalse(status.is_success)
        self.assertEqual(
            status.last_error_message, "HTTPBadGateway: 502 Test exception"
        )

    def test_update_assets_7(self, mock_esi):
        """when preload objects failed then report the error"""
        mock_esi.client = esi_client_stub

        with patch(
            MANAGERS_PATH + ".general.LocationManager.get_or_create_esi_async",
            spec=True,
        ) as m:
            exception = build_http_error(502, "Test exception")
            m.side_effect = exception
            with self.assertRaises(OSError):
                tasks.update_character_assets(self.character_1001.pk)

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.ASSETS
        )
        self.assertFalse(status.is_success)
        self.assertEqual(
            status.last_error_message, "HTTPBadGateway: 502 Test exception"
        )

    def test_update_assets_8(self, mock_esi):
        """when building the asset tree failed then report the error"""
        mock_esi.client = esi_client_stub

        with patch(MANAGERS_PATH + ".character_sections_1.logger") as m:
            exception = build_http_error(502, "Test exception")
            m.info.side_effect = exception
            with self.assertRaises(OSError):
                tasks.update_character_assets(self.character_1001.pk)

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.ASSETS
        )
        self.assertFalse(status.is_success)
        self.assertEqual(
            status.last_error_message, "HTTPBadGateway: 502 Test exception"
        )

    def test_update_assets_9(self, mock_esi):
        """when info from ESI has not change, then don't re-create asset tree"""
        mock_esi.client = esi_client_stub

        self.character_1001.reset_update_section(Character.UpdateSection.ASSETS)
        tasks.update_character_assets(self.character_1001.pk)
        asset = self.character_1001.assets.get(item_id=1100000000001)
        asset.name = "New Name"
        asset.save()
        tasks.update_character_assets(self.character_1001.pk)

        asset = self.character_1001.assets.get(item_id=1100000000001)
        self.assertEqual(asset.name, "New Name")

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.ASSETS
        )
        self.assertTrue(status.is_success)

    def test_update_assets_10(self, mock_esi):
        """when info from ESI has not change and update is forced, then re-create asset tree"""
        mock_esi.client = esi_client_stub

        self.character_1001.reset_update_section(Character.UpdateSection.ASSETS)
        tasks.update_character_assets(self.character_1001.pk)
        asset = self.character_1001.assets.get(item_id=1100000000001)
        asset.name = "New Name"
        asset.save()
        tasks.update_character_assets(self.character_1001.pk, force_update=True)

        asset = self.character_1001.assets.get(item_id=1100000000001)
        self.assertEqual(asset.name, "Parent Item 1")

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.ASSETS
        )
        self.assertTrue(status.is_success)


@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@patch(MANAGERS_PATH + ".character_sections_1.esi")
class TestUpdateCharacterContacts(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        load_entities()
        cls.character_1001 = create_memberaudit_character(1001)
        cls.token = (
            cls.character_1001.eve_character.character_ownership.user.token_set.first()
        )

    def test_update_ok(self, mock_esi):
        """when update succeeded then report update success"""
        mock_esi.client = esi_client_stub

        tasks.update_character_contacts(self.character_1001.pk)

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.CONTACTS
        )
        self.assertTrue(status.is_success)
        self.assertFalse(status.last_error_message)

    def test_detect_error(self, mock_esi):
        """when update failed then report the error"""
        exception = build_http_error(502, "Test exception")
        mock_esi.client.Contacts.get_characters_character_id_contacts_labels.side_effect = (
            exception
        )

        try:
            tasks.update_character_contacts(self.character_1001.pk)
        except Exception:
            status = self.character_1001.update_status_set.get(
                section=Character.UpdateSection.CONTACTS
            )
            self.assertFalse(status.is_success)
            self.assertEqual(
                status.last_error_message, "HTTPBadGateway: 502 Test exception"
            )
        else:
            self.assertTrue(False)  # Hack to ensure the test fails when it gets here


@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@patch(MANAGERS_PATH + ".character_sections_1.esi")
class TestUpdateCharacterContracts(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        load_entities()
        load_locations()
        cls.character_1001 = create_memberaudit_character(1001)
        cls.token = (
            cls.character_1001.eve_character.character_ownership.user.token_set.first()
        )

    def test_update_ok(self, mock_esi):
        """when update succeeded then report update success"""
        mock_esi.client = esi_client_stub

        tasks.update_character_contracts(self.character_1001.pk)

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.CONTRACTS
        )
        self.assertTrue(status.is_success)
        self.assertFalse(status.last_error_message)

    def test_detect_error(self, mock_esi):
        """when update failed then report the error"""
        exception = build_http_error(502, "Test exception")
        mock_esi.client.Contracts.get_characters_character_id_contracts.side_effect = (
            exception
        )

        try:
            tasks.update_character_contracts(self.character_1001.pk)
        except Exception:
            status = self.character_1001.update_status_set.get(
                section=Character.UpdateSection.CONTRACTS
            )
            self.assertFalse(status.is_success)
            self.assertEqual(
                status.last_error_message, "HTTPBadGateway: 502 Test exception"
            )
        else:
            self.assertTrue(False)  # Hack to ensure the test fails when it gets here


@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@patch(MANAGERS_PATH + ".character_sections_2.esi")
@patch(MANAGERS_PATH + ".general.esi")
class TestUpdateCharacterMails(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        load_entities()
        cls.character_1001 = create_memberaudit_character(1001)
        cls.token = (
            cls.character_1001.eve_character.character_ownership.user.token_set.first()
        )

    def test_update_ok(self, mock_esi_character, mock_esi_sections):
        """when update succeeded then report update success"""
        mock_esi_character.client = esi_client_stub
        mock_esi_sections.client = esi_client_stub

        tasks.update_character_mails(self.character_1001.pk)

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.MAILS
        )
        self.assertTrue(status.is_success)
        self.assertFalse(status.last_error_message)

    def test_detect_error(self, mock_esi_character, mock_esi_sections):
        """when update failed then report the error"""
        exception = build_http_error(502, "Test exception")
        mock_esi_character.client.Mail.get_characters_character_id_mail_lists.side_effect = (
            exception
        )
        mock_esi_sections.client.Mail.get_characters_character_id_mail_lists.side_effect = (
            exception
        )
        try:
            tasks.update_character_mails(self.character_1001.pk)
        except Exception:
            status = self.character_1001.update_status_set.get(
                section=Character.UpdateSection.MAILS
            )
            self.assertFalse(status.is_success)
            self.assertEqual(
                status.last_error_message, "HTTPBadGateway: 502 Test exception"
            )
        else:
            self.assertTrue(False)  # Hack to ensure the test fails when it gets here


@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@patch(MANAGERS_PATH + ".character_sections_3.esi")
class TestUpdateCharacterWalletJournal(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        load_entities()
        cls.character_1001 = create_memberaudit_character(1001)
        cls.token = (
            cls.character_1001.eve_character.character_ownership.user.token_set.first()
        )

    def test_update_ok(self, mock_esi):
        """when update succeeded then report update success"""
        mock_esi.client = esi_client_stub

        tasks.update_character_wallet_journal(self.character_1001.pk)

        status = self.character_1001.update_status_set.get(
            section=Character.UpdateSection.WALLET_JOURNAL
        )
        self.assertTrue(status.is_success)
        self.assertFalse(status.last_error_message)

    def test_detect_error(self, mock_esi):
        """when update failed then report the error"""
        exception = build_http_error(502, "Test exception")
        mock_esi.client.Wallet.get_characters_character_id_wallet_journal.side_effect = (
            exception
        )

        try:
            tasks.update_character_wallet_journal(self.character_1001.pk)
        except Exception:
            status = self.character_1001.update_status_set.get(
                section=Character.UpdateSection.WALLET_JOURNAL
            )
            self.assertFalse(status.is_success)
            self.assertEqual(
                status.last_error_message, "HTTPBadGateway: 502 Test exception"
            )
        else:
            self.assertTrue(False)  # Hack to ensure the test fails when it gets here


@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@patch(MANAGERS_PATH + ".character_sections_1.data_retention_cutoff", lambda: None)
@patch(MANAGERS_PATH + ".character_sections_2.data_retention_cutoff", lambda: None)
@patch(MANAGERS_PATH + ".character_sections_3.data_retention_cutoff", lambda: None)
@patch(MANAGERS_PATH + ".character_sections_1.esi", esi_stub)
@patch(MANAGERS_PATH + ".character_sections_2.esi", esi_stub)
@patch(MANAGERS_PATH + ".character_sections_3.esi", esi_stub)
@patch(MANAGERS_PATH + ".general.esi", esi_stub)
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestCharacterUpdateFull(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        load_entities()
        load_locations()

    def setUp(self) -> None:
        self.character_1001 = create_memberaudit_character(1001)

    @skipIf(TOX_IS_RUNNING, "does not work with tox")
    def test_should_update_normally(self):
        """can update from scratch"""
        # when
        result = tasks.update_character(self.character_1001.pk)
        # then
        self.assertTrue(result)
        self.assertTrue(self.character_1001.is_update_status_ok())

    @patch(TASKS_PATH + ".Character.update_loyalty", spec=True)
    def test_should_update_normal_section_only_when_stale(self, update_loyalty):
        # given
        CharacterUpdateStatus.objects.create(
            character=self.character_1001,
            section=Character.UpdateSection.LOYALTY,
            is_success=True,
            started_at=now() - dt.timedelta(seconds=30),
            finished_at=now(),
        )
        # when
        tasks.update_character(self.character_1001.pk)
        # then
        self.assertFalse(update_loyalty.called)

    @patch(TASKS_PATH + ".update_character_mails", spec=True)
    def test_should_update_special_section_only_when_stale(
        self, mock_update_character_mails
    ):
        # given
        CharacterUpdateStatus.objects.create(
            character=self.character_1001,
            section=Character.UpdateSection.MAILS,
            is_success=True,
            started_at=now() - dt.timedelta(seconds=30),
            finished_at=now(),
        )
        # when
        tasks.update_character(self.character_1001.pk)
        # then
        self.assertFalse(mock_update_character_mails.apply_async.called)

    def test_should_update_stale_sections_only_3(self):
        """When generic section has recently been updated and force_update is called
        then update again
        """
        # given
        section: CharacterUpdateStatus = CharacterUpdateStatus.objects.create(
            character=self.character_1001,
            section=Character.UpdateSection.SKILLS.value,
            is_success=True,
            started_at=now() - dt.timedelta(seconds=30),
            finished_at=now(),
        )
        last_finished = section.finished_at
        # when
        tasks.update_character(self.character_1001.pk, force_update=True)
        # then
        section.refresh_from_db()
        self.assertGreater(section.finished_at, last_finished)

    def test_should_do_nothing_when_not_required(self):
        # given
        for section in Character.UpdateSection.values:
            CharacterUpdateStatus.objects.create(
                character=self.character_1001,
                section=section,
                is_success=True,
                started_at=now() - dt.timedelta(seconds=30),
                finished_at=now(),
            )
        # when
        result = tasks.update_character(self.character_1001.pk)
        # then
        self.assertFalse(result)

    def test_can_do_forced_update(self):
        # when
        result = tasks.update_character(self.character_1001.pk, force_update=True)
        # then
        self.assertTrue(result)
        self.assertTrue(self.character_1001.is_update_status_ok())

    def test_skip_update_for_orphans(self):
        # given
        character = create_character(EveCharacter.objects.get(character_id=1121))
        # when
        result = tasks.update_character(character.pk)
        # then
        self.assertFalse(result)
        self.assertIsNone(character.is_update_status_ok())


@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@patch(MANAGERS_PATH + ".character_sections_1.data_retention_cutoff", lambda: None)
@patch(MANAGERS_PATH + ".character_sections_2.data_retention_cutoff", lambda: None)
@patch(MANAGERS_PATH + ".character_sections_3.data_retention_cutoff", lambda: None)
@patch(MANAGERS_PATH + ".character_sections_1.esi", esi_error_stub)
@patch(MANAGERS_PATH + ".character_sections_2.esi", esi_error_stub)
@patch(MANAGERS_PATH + ".character_sections_3.esi", esi_error_stub)
@patch(MANAGERS_PATH + ".general.esi", esi_error_stub)
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestCharacterUpdateErrorReporting(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        load_entities()
        load_locations()

    def setUp(self) -> None:
        self.character_1001 = create_memberaudit_character(1001)

    def test_should_report_errors_during_updates(self):
        # when
        with self.assertRaises(OSError):  # raised when trying to fetch attributes
            tasks.update_character(self.character_1001.pk)
        # then
        self.assertFalse(self.character_1001.is_update_status_ok())
        status = self.character_1001.update_status_set.filter(
            character=self.character_1001, is_success=False
        ).first()
        self.assertEqual(
            status.last_error_message, "HTTPBadGateway: 502 Test exception"
        )
        self.assertTrue(status.finished_at)


@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@patch(TASKS_PATH + ".Location.objects.structure_update_or_create_esi", spec=True)
class TestUpdateStructureEsi(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_entities()
        cls.character = create_memberaudit_character(1001)
        cls.token = (
            cls.character.eve_character.character_ownership.user.token_set.first()
        )

    def test_normal(self, mock_structure_update_or_create_esi):
        """When ESI status is ok, then create MailEntity"""
        mock_structure_update_or_create_esi.return_value = None
        try:
            tasks.update_structure_esi(id=1000000000001, token_pk=self.token.pk)
        except Exception as ex:
            self.fail(f"Unexpected exception occurred: {ex}")

    def test_invalid_token(self, mock_structure_update_or_create_esi):
        """When called with invalid token, raise exception"""
        mock_structure_update_or_create_esi.side_effect = EsiOffline

        with self.assertRaises(Token.DoesNotExist):
            tasks.update_structure_esi(
                id=1000000000001, token_pk=generate_invalid_pk(Token)
            )

    def test_esi_status_1(self, mock_structure_update_or_create_esi):
        """When ESI is offline, then retry"""
        mock_structure_update_or_create_esi.side_effect = EsiOffline

        with self.assertRaises(CeleryRetry):
            tasks.update_structure_esi(id=1000000000001, token_pk=self.token.pk)

    def test_esi_status_2(self, mock_structure_update_or_create_esi):
        """When ESI error limit reached, then retry"""
        mock_structure_update_or_create_esi.side_effect = EsiErrorLimitExceeded(5)

        with self.assertRaises(CeleryRetry):
            tasks.update_structure_esi(id=1000000000001, token_pk=self.token.pk)


@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@patch(TASKS_PATH + ".MailEntity.objects.update_or_create_esi", spec=True)
class TestUpdateMailEntityEsi(TestCaseTasks):
    def test_normal(self, mock_update_or_create_esi):
        """When ESI status is ok, then create MailEntity"""
        mock_update_or_create_esi.return_value = None
        try:
            tasks.update_mail_entity_esi(1001)
        except Exception:
            self.fail("Unexpected exception occurred")

    def test_esi_status_1(self, mock_update_or_create_esi):
        """When ESI is offline, then abort"""
        mock_update_or_create_esi.side_effect = EsiOffline

        with self.assertRaises(EsiOffline):
            tasks.update_mail_entity_esi(1001)

    def test_esi_status_2(self, mock_update_or_create_esi):
        """When ESI error limit reached, then abort"""
        mock_update_or_create_esi.side_effect = EsiErrorLimitExceeded(5)

        with self.assertRaises(EsiErrorLimitExceeded):
            tasks.update_mail_entity_esi(1001)


@patch(MANAGERS_PATH + ".general.fetch_esi_status", lambda: EsiStatus(True, 99, 60))
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestUpdateCharactersDoctrines(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_entities()

    def setUp(self) -> None:
        self.character_1001 = create_memberaudit_character(1001)

    @patch(MODELS_PATH + ".characters.Character.update_skill_sets")
    def test_normal(self, mock_update_skill_sets):
        tasks.update_characters_skill_checks()
        self.assertTrue(mock_update_skill_sets.called)


@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestDeleteCharacter(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_entities()

    def test_should_delete_a_character(self):
        # given
        character_1001 = create_memberaudit_character(1001)
        # when
        tasks.delete_character.delay(character_1001.pk)
        # then
        self.assertFalse(Character.objects.filter(pk=character_1001.pk).exists())


@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestExportData(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_entities()
        cls.character = create_memberaudit_character(1001)

    @patch(TASKS_PATH + ".data_exporters.export_topic_to_archive", spec=True)
    def test_should_export_all_topics(self, mock_export_topic_to_file):
        # when
        tasks.export_data()
        # then
        called_topics = [
            call[1]["topic"] for call in mock_export_topic_to_file.call_args_list
        ]
        self.assertEqual(len(called_topics), 3)
        self.assertSetEqual(
            set(called_topics), {"contract", "contract-item", "wallet-journal"}
        )

    @patch(TASKS_PATH + ".data_exporters.export_topic_to_archive", spec=True)
    def test_should_export_wallet_journal(self, mock_export_topic_to_file):
        # given
        user = self.character.user
        # when
        tasks.export_data_for_topic(topic="abc", user_pk=user.pk)
        # then
        self.assertTrue(mock_export_topic_to_file.called)
        _, kwargs = mock_export_topic_to_file.call_args
        self.assertEqual(kwargs["topic"], "abc")


class TestUpdateComplianceGroupDesignations(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_entities()

    @patch(TASKS_PATH + ".ComplianceGroupDesignation.objects.update_user", spec=True)
    def test_should_update_for_user(self, mock_update_user):
        # given
        user, _ = create_user_from_evecharacter(
            1001,
            permissions=["memberaudit.basic_access"],
            scopes=Character.get_esi_scopes(),
        )
        # when
        tasks.update_compliance_groups_for_user(user.pk)
        # then
        self.assertTrue(mock_update_user.called)


@patch(TASKS_PATH + ".update_character", spec=True)
class TestUpdateAllCharacters(TestCaseTasks):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        load_entities()
        load_locations()

    def test_should_update_all_enabled_characters(self, mock_update_character):
        # given
        character_1001 = create_memberaudit_character(1001)
        character_1002 = create_memberaudit_character(1002)
        character_1003 = create_memberaudit_character(1003)
        character_1003.is_disabled = True
        character_1003.save()
        # when
        tasks.update_all_characters()
        # then
        self.assertTrue(mock_update_character.apply_async.called)
        called_pks = {
            o[1]["kwargs"]["character_pk"]
            for o in mock_update_character.apply_async.call_args_list
        }
        self.assertSetEqual(called_pks, {character_1001.pk, character_1002.pk})

    def test_should_disable_orphaned_characters(self, mock_update_character):
        # given
        character_1001 = create_memberaudit_character(1001)
        eve_character_1002 = EveCharacter.objects.get(character_id=1002)
        character_1002 = create_character(eve_character_1002)
        # when
        tasks.update_all_characters()
        # then
        character_1001.refresh_from_db()
        self.assertFalse(character_1001.is_disabled)
        character_1002.refresh_from_db()
        self.assertTrue(character_1002.is_disabled)


@patch(TASKS_PATH + ".EveEntity.objects.update_from_esi_by_id", spec=True)
class TestUpdateUnresolvedEveEntities(TestCaseTasks):
    def test_should_not_attempt_to_update_when_no_unresolved_entities(
        self, mock_update_from_esi_by_id
    ):
        # given
        create_eve_entity(id=1, name="alpha")
        # when
        tasks.update_unresolved_eve_entities()
        # then
        self.assertFalse(mock_update_from_esi_by_id.called)

    def test_should_update_unresolved_entities(self, mock_update_from_esi_by_id):
        # given
        create_eve_entity(id=1)
        # when
        tasks.update_unresolved_eve_entities()
        # then
        self.assertTrue(mock_update_from_esi_by_id.called)
        args, _ = mock_update_from_esi_by_id.call_args
        self.assertEqual(list(args[0]), [1])


@patch(TASKS_PATH + ".check_character_consistency", spec=True)
class TestCheckCharacterConsistency(TestCaseTasks):
    def test_should_run_checks(self, mock_check_character_consistency):
        # given
        load_entities()
        character = create_memberaudit_character(1001)
        # when
        tasks.check_character_consistency(character.pk)
        # then
        self.assertTrue(mock_check_character_consistency.called)
