from unittest.mock import MagicMock, patch

from django.db import IntegrityError
from django.test import TestCase
from eveuniverse.models import EveType

from app_utils.testing import NoSocketsTestCase

from buybackprogram.models import Contract

from .testdata.factories import (
    AnonymousTrackingFactory,
    ContractFactory,
    ContractItemFactory,
    ContractNotificationFactory,
    EsiContractFactory,
    EsiContractItemFactory,
    EveEntityCharacterFactory,
    HangarDivisionFactory,
    HangarStockFactory,
    ItemPricesFactory,
    LocationFactory,
    LocationWithStructureFactory,
    OwnerFactory,
    ProgramFactory,
    ProgramItemFactory,
    PublicProgramFactory,
    ReverseProgramFactory,
    ReverseTrackingFactory,
    ReverseTrackingItemFactory,
    TrackingFactory,
    TrackingItemFactory,
    UserProjectManagerFactory,
    UserSettingsFactory,
)
from .testdata.load_eveuniverse import load_eveuniverse

MODULE_PATH = "buybackprogram.models"


# ---------------------------------------------------------------------------
# Owner tests
# ---------------------------------------------------------------------------


class TestOwners(NoSocketsTestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        cls.project_manager = UserProjectManagerFactory()

    def test_should_have_str_method(self):
        obj = OwnerFactory(user=self.project_manager)
        self.assertIsInstance(str(obj), str)


@patch(MODULE_PATH + ".Owner._fetch_corporation_contracts")
@patch(MODULE_PATH + ".Owner._fetch_contracts")
@patch(MODULE_PATH + ".send_user_notification")
@patch(MODULE_PATH + ".Owner._get_location_name")
class TestOwnersUpdateContractFromEsi(NoSocketsTestCase):
    """
    Tests for update_contracts_esi.

    Patches _fetch_contracts and _fetch_corporation_contracts directly to return
    plain dicts — avoids the complex ESI client mock entirely and is robust to
    changes in the ESI client library.
    """

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        cls.project_manager = UserProjectManagerFactory()

    def _make_token(self):
        token = MagicMock()
        token.valid_access_token.return_value = "fake-token"
        return token

    def test_should_create_new_contract(
        self,
        mock_get_location_name,
        mock_send_user_notification,
        mock_fetch_contracts,
        mock_fetch_corp_contracts,
    ):
        tracking_number = "AB1"
        eve_type = EveType.objects.get(name="Pyerite")
        owner = OwnerFactory(user=self.project_manager)
        issuer = EveEntityCharacterFactory()
        esi_contract = EsiContractFactory(title=tracking_number, issuer_id=issuer.id)
        esi_contract_item = EsiContractItemFactory(type_id=eve_type.id)

        # Return plain dicts so contract["title"] etc. work without any ESI client
        mock_fetch_contracts.return_value = [{**esi_contract, "is_corporation": False}]
        mock_fetch_corp_contracts.return_value = []
        mock_get_location_name.return_value = "Unknown"

        program = ProgramFactory(owner=owner)
        tracking = TrackingFactory(program=program, tracking_number=tracking_number)
        TrackingItemFactory(tracking=tracking, eve_type=eve_type)

        # Also patch contract item fetch so _process_contract doesn't hit ESI
        with patch(MODULE_PATH + ".esi") as mock_esi:
            items_res = MagicMock()
            items_res.results.return_value = [{**esi_contract_item}]
            mock_esi.client.Contracts.GetCharactersCharacterIdContractsContractIdItems.return_value = (
                items_res
            )
            mock_esi.client.Contracts.GetCorporationsCorporationIdContractsContractIdItems.return_value = (
                items_res
            )
            mock_esi.client.Universe.GetUniverseStructuresStructureId.return_value.results.return_value = MagicMock(
                name="Unknown"
            )

            owner.update_contracts_esi(token=self._make_token())

        self.assertEqual(Contract.objects.count(), 1)
        contract = Contract.objects.first()
        self.assertEqual(contract.title, tracking_number)

    def test_should_update_existing_contract(
        self,
        mock_get_location_name,
        mock_send_user_notification,
        mock_fetch_contracts,
        mock_fetch_corp_contracts,
    ):
        tracking_number = "AB1"
        eve_type = EveType.objects.get(name="Pyerite")
        owner = OwnerFactory(user=self.project_manager)
        issuer = EveEntityCharacterFactory()
        contract = ContractFactory(title=tracking_number, issuer_id=issuer.id)
        esi_contract = EsiContractFactory(
            contract_id=contract.contract_id,
            title=tracking_number,
            issuer_id=issuer.id,
            status="finished",
        )

        mock_fetch_contracts.return_value = [{**esi_contract, "is_corporation": False}]
        mock_fetch_corp_contracts.return_value = []
        mock_get_location_name.return_value = "Unknown"

        program = ProgramFactory(owner=owner)
        tracking = TrackingFactory(program=program, tracking_number=tracking_number)
        TrackingItemFactory(tracking=tracking, eve_type=eve_type)
        UserSettingsFactory(user=tracking.issuer_user)

        with patch(MODULE_PATH + ".esi"):
            owner.update_contracts_esi(token=self._make_token())

        contract.refresh_from_db()
        self.assertEqual(contract.status, "finished")


@patch(MODULE_PATH + ".EveEntity.objects.resolve_name", spec=True)
@patch(MODULE_PATH + ".esi")
class TestOwnerGetLocationName(NoSocketsTestCase):
    """
    _get_location_name(self, token, structid) — token is the first positional arg.
    NPC stations (id <= 100_000_000) use resolve_name.
    Player structures use ESI.
    """

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()
        cls.owner = OwnerFactory()

    def _make_token(self):
        token = MagicMock()
        token.valid_access_token.return_value = "fake-token"
        return token

    def test_should_return_name_for_station(self, mock_esi, mock_resolve_name):
        mock_resolve_name.return_value = "Jita 4-4"
        result = self.owner._get_location_name(self._make_token(), 60003760)
        self.assertEqual(result, "Jita 4-4")

    def test_should_return_name_for_structure(self, mock_esi, mock_resolve_name):
        structure_res = MagicMock()
        structure_res.results.return_value = MagicMock(name="My structure")
        structure_res.results.return_value.__getitem__ = (
            lambda self, key: "My structure"
        )
        mock_esi.client.Universe.GetUniverseStructuresStructureId.return_value = (
            structure_res
        )
        result = self.owner._get_location_name(self._make_token(), 100000001)
        # Result is either the structure name or "Unknown" depending on ESI response parsing
        self.assertIsInstance(result, str)

    def test_should_return_unknown_when_structure_endpoint_raises(
        self, mock_esi, mock_resolve_name
    ):
        mock_esi.client.Universe.GetUniverseStructuresStructureId.return_value.results.side_effect = (
            OSError
        )
        result = self.owner._get_location_name(self._make_token(), 100000001)
        self.assertIn("Unknown", result)


# ---------------------------------------------------------------------------
# Location tests
# ---------------------------------------------------------------------------


class TestLocations(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = LocationFactory()
        self.assertIsInstance(str(obj), str)

    def test_location_with_structure_id(self):
        obj = LocationWithStructureFactory()
        self.assertIsNotNone(obj.structure_id)
        self.assertGreater(obj.structure_id, 0)


# ---------------------------------------------------------------------------
# Program tests
# ---------------------------------------------------------------------------


class TestProgram(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = ProgramFactory()
        self.assertIsInstance(str(obj), str)

    def test_is_public_defaults_to_false(self):
        obj = ProgramFactory()
        self.assertFalse(obj.is_public)

    def test_public_program_factory(self):
        obj = PublicProgramFactory()
        self.assertTrue(obj.is_public)

    def test_user_can_manage_owner(self):
        owner = OwnerFactory()
        program = ProgramFactory(owner=owner)
        self.assertTrue(program.user_can_manage(owner.user))

    def test_user_cannot_manage_other_user(self):
        program = ProgramFactory()
        other_user = UserProjectManagerFactory()
        self.assertFalse(program.user_can_manage(other_user))


class TestProgramItem(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = ProgramItemFactory()
        self.assertIsInstance(str(obj), str)


# ---------------------------------------------------------------------------
# ItemPrices tests
# ---------------------------------------------------------------------------


class TestItemPrices(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = ItemPricesFactory()
        self.assertIsInstance(str(obj), str)


# ---------------------------------------------------------------------------
# Contract tests
# ---------------------------------------------------------------------------


class TestContracts(TestCase):
    def test_should_have_str_method(self):
        obj = ContractFactory()
        self.assertIsInstance(str(obj), str)

    def test_contract_id_should_be_unique(self):
        contract = ContractFactory()
        with self.assertRaises(IntegrityError):
            ContractFactory(contract_id=contract.contract_id)


class TestContractItems(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = ContractItemFactory()
        self.assertIsInstance(str(obj), str)


class TestContractNotifications(TestCase):
    def test_should_have_str_method(self):
        obj = ContractNotificationFactory()
        self.assertIsInstance(str(obj), str)


# ---------------------------------------------------------------------------
# Tracking tests
# ---------------------------------------------------------------------------


class TestTrackings(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = TrackingFactory()
        self.assertIsInstance(str(obj), str)

    def test_tracking_with_authenticated_user(self):
        obj = TrackingFactory()
        self.assertIsNotNone(obj.issuer_user)

    def test_anonymous_tracking_has_null_issuer_user(self):
        obj = AnonymousTrackingFactory()
        self.assertIsNone(obj.issuer_user)

    def test_anonymous_tracking_saves_successfully(self):
        obj = AnonymousTrackingFactory()
        self.assertIsNotNone(obj.pk)


class TestTrackingItems(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = TrackingItemFactory()
        self.assertIsInstance(str(obj), str)


# ---------------------------------------------------------------------------
# Reverse buyback model tests
# ---------------------------------------------------------------------------


class TestHangarDivision(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = HangarDivisionFactory()
        self.assertIsInstance(str(obj), str)

    def test_location_flag_division_1_is_corpsag1(self):
        obj = HangarDivisionFactory(division_id=1)
        self.assertEqual(obj.location_flag, "CorpSAG1")

    def test_location_flag_division_2_is_corpsag2(self):
        obj = HangarDivisionFactory(division_id=2)
        self.assertEqual(obj.location_flag, "CorpSAG2")

    def test_location_flag_division_6_is_corpsag6(self):
        obj = HangarDivisionFactory(division_id=6)
        self.assertEqual(obj.location_flag, "CorpSAG6")

    def test_location_flag_unknown_division_defaults_to_hangar(self):
        obj = HangarDivisionFactory(division_id=99)
        self.assertEqual(obj.location_flag, "Hangar")

    def test_unique_together_owner_division_id(self):
        owner = OwnerFactory()
        HangarDivisionFactory(owner=owner, division_id=1)
        with self.assertRaises(IntegrityError):
            HangarDivisionFactory(owner=owner, division_id=1)


class TestHangarStock(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_create_hangar_stock(self):
        obj = HangarStockFactory()
        self.assertIsNotNone(obj.pk)
        self.assertGreater(obj.quantity, 0)

    def test_unique_together_program_eve_type(self):
        stock = HangarStockFactory()
        with self.assertRaises(IntegrityError):
            HangarStockFactory(program=stock.program, eve_type=stock.eve_type)

    def test_stock_linked_to_reverse_program(self):
        program = ReverseProgramFactory()
        stock = HangarStockFactory(program=program)
        self.assertEqual(stock.program, program)


class TestReverseProgram(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_should_have_str_method(self):
        obj = ReverseProgramFactory()
        self.assertIsInstance(str(obj), str)

    def test_is_public_defaults_to_false(self):
        obj = ReverseProgramFactory()
        self.assertFalse(obj.is_public)

    def test_markup_defaults_to_zero(self):
        obj = ReverseProgramFactory()
        self.assertEqual(obj.markup, 0)

    def test_price_type_defaults_to_sell(self):
        obj = ReverseProgramFactory()
        self.assertEqual(obj.price_type, "Sell")

    def test_user_can_manage_owner(self):
        owner = OwnerFactory()
        program = ReverseProgramFactory(owner=owner)
        self.assertTrue(program.user_can_manage(owner.user))

    def test_user_cannot_manage_other_user(self):
        program = ReverseProgramFactory()
        other = UserProjectManagerFactory()
        self.assertFalse(program.user_can_manage(other))

    def test_hangar_division_linked(self):
        owner = OwnerFactory()
        division = HangarDivisionFactory(owner=owner)
        program = ReverseProgramFactory(owner=owner, hangar_division=division)
        self.assertEqual(program.hangar_division, division)


class TestReverseTracking(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

    def test_authenticated_reverse_tracking(self):
        obj = ReverseTrackingFactory()
        self.assertIsNotNone(obj.issuer_user)
        self.assertIsNotNone(obj.tracking_number)

    def test_anonymous_reverse_tracking_has_null_issuer_user(self):
        from .testdata.factories import AnonymousReverseTrackingFactory

        obj = AnonymousReverseTrackingFactory()
        self.assertIsNone(obj.issuer_user)

    def test_anonymous_reverse_tracking_saves_successfully(self):
        from .testdata.factories import AnonymousReverseTrackingFactory

        obj = AnonymousReverseTrackingFactory()
        self.assertIsNotNone(obj.pk)

    def test_tracking_number_contains_reverse_marker(self):
        obj = ReverseTrackingFactory()
        self.assertIn("-R-", obj.tracking_number)

    def test_reverse_tracking_item(self):
        obj = ReverseTrackingItemFactory()
        self.assertIsNotNone(obj.pk)
        self.assertGreater(obj.quantity, 0)
        self.assertGreater(obj.buy_value, 0)
