"""
Tests specific to the reverse buyback feature:
- HangarDivision.location_flag property
- update_hangar_divisions_esi — ESI parsing
- update_hangar_esi — asset filtering by location + division flag
- ReverseProgram.user_can_manage
- reverse_program_calculate view — stock check, tracking creation, anonymous support
"""

import json
from unittest.mock import MagicMock, patch

from django.test import Client, TestCase
from django.urls import reverse

from app_utils.testing import NoSocketsTestCase

from buybackprogram.models import HangarDivision, HangarStock, ReverseTracking

from .testdata.factories import (
    HangarDivisionFactory,
    HangarStockFactory,
    ItemPricesFactory,
    LocationWithStructureFactory,
    OwnerFactory,
    PublicReverseProgramFactory,
    ReverseProgramFactory,
    UserIssuerFactory,
    UserProjectManagerFactory,
)
from .testdata.load_eveuniverse import load_eveuniverse

MODULE_PATH = "buybackprogram.models"


# ---------------------------------------------------------------------------
# HangarDivision location_flag property
# ---------------------------------------------------------------------------


class TestHangarDivisionLocationFlag(TestCase):
    # Correct mapping: division 1 = CorpSAG1, division 6 = CorpSAG6
    # (no Hangar flag for corp divisions in player structures)
    EXPECTED_FLAGS = {
        1: "CorpSAG1",
        2: "CorpSAG2",
        3: "CorpSAG3",
        4: "CorpSAG4",
        5: "CorpSAG5",
        6: "CorpSAG6",
    }

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

    def test_all_division_flags(self):
        owner = OwnerFactory()
        for div_id, expected_flag in self.EXPECTED_FLAGS.items():
            with self.subTest(division_id=div_id):
                obj = HangarDivisionFactory(owner=owner, division_id=div_id)
                self.assertEqual(obj.location_flag, expected_flag)

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


# ---------------------------------------------------------------------------
# update_hangar_divisions_esi
# ---------------------------------------------------------------------------


@patch(MODULE_PATH + ".esi")
class TestUpdateHangarDivisionsEsi(NoSocketsTestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

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

    def _make_divisions_response(self, hangars):
        """Build a mock divisions ESI response with named hangar divisions."""
        response = MagicMock()
        hangar_objs = []
        for div_id, name in hangars.items():
            h = MagicMock()
            h.division = div_id
            h.name = name
            hangar_objs.append(h)
        response.hangar = hangar_objs
        return response

    def test_creates_hangar_divisions_from_esi(self, mock_esi):
        owner = OwnerFactory()
        mock_esi.client.Corporation.GetCorporationsCorporationIdDivisions.return_value.results.return_value = self._make_divisions_response(
            {1: "Minerals", 2: "Ships", 3: ""}
        )
        owner.update_hangar_divisions_esi(token=self._make_token())
        self.assertEqual(HangarDivision.objects.filter(owner=owner).count(), 3)
        div1 = HangarDivision.objects.get(owner=owner, division_id=1)
        self.assertEqual(div1.name, "Minerals")

    def test_unnamed_division_gets_default_name(self, mock_esi):
        owner = OwnerFactory()
        mock_esi.client.Corporation.GetCorporationsCorporationIdDivisions.return_value.results.return_value = self._make_divisions_response(
            {2: ""}
        )
        owner.update_hangar_divisions_esi(token=self._make_token())
        div2 = HangarDivision.objects.get(owner=owner, division_id=2)
        self.assertEqual(div2.name, "2nd Division")

    def test_division_1_unnamed_gets_default_name(self, mock_esi):
        owner = OwnerFactory()
        mock_esi.client.Corporation.GetCorporationsCorporationIdDivisions.return_value.results.return_value = self._make_divisions_response(
            {1: ""}
        )
        owner.update_hangar_divisions_esi(token=self._make_token())
        div1 = HangarDivision.objects.get(owner=owner, division_id=1)
        self.assertEqual(div1.name, "1st Division")

    def test_upserts_existing_division(self, mock_esi):
        owner = OwnerFactory()
        HangarDivisionFactory(owner=owner, division_id=1, name="Old Name")
        mock_esi.client.Corporation.GetCorporationsCorporationIdDivisions.return_value.results.return_value = self._make_divisions_response(
            {1: "New Name"}
        )
        owner.update_hangar_divisions_esi(token=self._make_token())
        div1 = HangarDivision.objects.get(owner=owner, division_id=1)
        self.assertEqual(div1.name, "New Name")

    def test_304_not_modified_is_handled_gracefully(self, mock_esi):
        """ESI 304 responses must not crash — data from prior sync is still valid."""

        class HTTPNotModified(Exception):
            pass

        owner = OwnerFactory()
        mock_esi.client.Corporation.GetCorporationsCorporationIdDivisions.return_value.results.side_effect = HTTPNotModified(
            "304"
        )
        # Should not raise
        owner.update_hangar_divisions_esi(token=self._make_token())


# ---------------------------------------------------------------------------
# update_hangar_esi
# ---------------------------------------------------------------------------


@patch(MODULE_PATH + ".esi")
class TestUpdateHangarEsi(NoSocketsTestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        load_eveuniverse()

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

    def _make_asset(self, type_id, quantity, location_id, location_flag):
        asset = MagicMock()
        asset.type_id = type_id
        asset.quantity = quantity
        asset.location_id = location_id
        asset.location_flag = location_flag
        return asset

    def test_stock_populated_for_matching_location_and_division(self, mock_esi):
        from eveuniverse.models import EveType

        eve_type = EveType.objects.filter(published=True).first()
        owner = OwnerFactory()
        # Division 1 = CorpSAG1 in the corrected mapping
        division = HangarDivisionFactory(owner=owner, division_id=1)
        location = LocationWithStructureFactory(owner=owner)
        program = ReverseProgramFactory(owner=owner, hangar_division=division)
        program.location.set([location])

        # Use CorpSAG1 to match division 1
        asset = self._make_asset(eve_type.id, 500, location.structure_id, "CorpSAG1")
        mock_esi.client.Assets.GetCorporationsCorporationIdAssets.return_value.results.return_value = [
            asset
        ]

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

        stock = HangarStock.objects.filter(program=program, eve_type=eve_type).first()
        self.assertIsNotNone(stock)
        self.assertEqual(stock.quantity, 500)

    def test_stock_not_populated_for_wrong_division(self, mock_esi):
        from eveuniverse.models import EveType

        eve_type = EveType.objects.filter(published=True).first()
        owner = OwnerFactory()
        # Division 2 = CorpSAG2
        division = HangarDivisionFactory(owner=owner, division_id=2)
        location = LocationWithStructureFactory(owner=owner)
        program = ReverseProgramFactory(owner=owner, hangar_division=division)
        program.location.set([location])

        # Asset is in CorpSAG1 (division 1), program expects CorpSAG2 (division 2)
        asset = self._make_asset(eve_type.id, 100, location.structure_id, "CorpSAG1")
        mock_esi.client.Assets.GetCorporationsCorporationIdAssets.return_value.results.return_value = [
            asset
        ]

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

        stock = HangarStock.objects.filter(program=program).first()
        self.assertIsNone(stock)

    def test_stock_not_populated_for_wrong_location(self, mock_esi):
        from eveuniverse.models import EveType

        eve_type = EveType.objects.filter(published=True).first()
        owner = OwnerFactory()
        division = HangarDivisionFactory(owner=owner, division_id=1)
        location = LocationWithStructureFactory(owner=owner, structure_id=1111111111)
        location.asset_location_id = (
            1111111111  # prevent auto-discovery from matching wrong structure
        )
        location.save()
        program = ReverseProgramFactory(owner=owner, hangar_division=division)
        program.location.set([location])

        # Asset is at a different structure
        asset = self._make_asset(eve_type.id, 100, 9999999999, "CorpSAG1")
        mock_esi.client.Assets.GetCorporationsCorporationIdAssets.return_value.results.return_value = [
            asset
        ]

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

        stock = HangarStock.objects.filter(program=program).first()
        self.assertIsNone(stock)

    def test_existing_stock_cleared_before_update(self, mock_esi):
        """Old stock entries are deleted before re-syncing."""
        from eveuniverse.models import EveType

        owner = OwnerFactory()
        division = HangarDivisionFactory(owner=owner, division_id=1)
        location = LocationWithStructureFactory(owner=owner)
        program = ReverseProgramFactory(owner=owner, hangar_division=division)
        program.location.set([location])

        # Create stale stock entry
        old_type = EveType.objects.filter(published=True).first()
        HangarStockFactory(program=program, eve_type=old_type, quantity=999)

        # ESI now returns no assets
        mock_esi.client.Assets.GetCorporationsCorporationIdAssets.return_value.results.return_value = (
            []
        )

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

        self.assertEqual(HangarStock.objects.filter(program=program).count(), 0)

    def test_skips_if_no_reverse_programs(self, mock_esi):
        owner = OwnerFactory()
        # No reverse programs for this owner
        owner.update_hangar_esi(token=self._make_token())
        # ESI should not be called at all
        mock_esi.client.Assets.GetCorporationsCorporationIdAssets.assert_not_called()

    def test_304_not_modified_handled_gracefully(self, mock_esi):
        class HTTPNotModified(Exception):
            pass

        owner = OwnerFactory()
        division = HangarDivisionFactory(owner=owner)
        ReverseProgramFactory(owner=owner, hangar_division=division)

        mock_esi.client.Assets.GetCorporationsCorporationIdAssets.return_value.results.side_effect = HTTPNotModified(
            "304"
        )
        # Should not raise
        owner.update_hangar_esi(token=self._make_token())


# ---------------------------------------------------------------------------
# reverse_program_calculate — tracking creation
# ---------------------------------------------------------------------------


class TestReverseProgramCalculateTracking(TestCase):
    """POST to reverse_program_calculate creates ReverseTracking records."""

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

    def setUp(self):
        self.client = Client(raise_request_exception=False)

    def _post_items(self, program, items_str):
        payload = []
        from eveuniverse.models import EveType

        for name in items_str:
            eve_type = EveType.objects.filter(name=name, published=True).first()
            if eve_type:
                payload.append({"type_id": eve_type.id, "quantity": 10})
        return self.client.post(
            reverse(
                "buybackprogram:reverse_program_calculate",
                kwargs={"program_pk": program.pk},
            ),
            data={"selected_items": json.dumps(payload)},
        )

    def test_anonymous_submission_creates_tracking_with_null_user(self):
        from eveuniverse.models import EveType

        program = PublicReverseProgramFactory()
        eve_type = EveType.objects.filter(published=True).first()
        HangarStockFactory(program=program, eve_type=eve_type, quantity=100)
        ItemPricesFactory(eve_type=eve_type)

        payload = [{"type_id": eve_type.id, "quantity": 10}]
        response = self.client.post(
            reverse(
                "buybackprogram:reverse_program_calculate",
                kwargs={"program_pk": program.pk},
            ),
            data={"selected_items": json.dumps(payload)},
        )
        self.assertNotEqual(response.status_code, 302)
        tracking = ReverseTracking.objects.filter(program=program).first()
        self.assertIsNotNone(tracking)
        self.assertIsNone(tracking.issuer_user)

    def test_authenticated_submission_creates_tracking_with_user(self):
        from eveuniverse.models import EveType

        user = UserIssuerFactory()
        self.client.force_login(user)
        program = PublicReverseProgramFactory()
        eve_type = EveType.objects.filter(published=True).first()
        HangarStockFactory(program=program, eve_type=eve_type, quantity=100)
        ItemPricesFactory(eve_type=eve_type)

        payload = [{"type_id": eve_type.id, "quantity": 10}]
        response = self.client.post(
            reverse(
                "buybackprogram:reverse_program_calculate",
                kwargs={"program_pk": program.pk},
            ),
            data={"selected_items": json.dumps(payload)},
        )
        self.assertNotEqual(response.status_code, 302)
        tracking = ReverseTracking.objects.filter(program=program).first()
        self.assertIsNotNone(tracking)
        self.assertEqual(tracking.issuer_user, user)

    def test_tracking_number_contains_reverse_marker(self):
        from eveuniverse.models import EveType

        program = PublicReverseProgramFactory()
        eve_type = EveType.objects.filter(published=True).first()
        HangarStockFactory(program=program, eve_type=eve_type, quantity=100)
        ItemPricesFactory(eve_type=eve_type)

        payload = [{"type_id": eve_type.id, "quantity": 10}]
        self.client.post(
            reverse(
                "buybackprogram:reverse_program_calculate",
                kwargs={"program_pk": program.pk},
            ),
            data={"selected_items": json.dumps(payload)},
        )
        tracking = ReverseTracking.objects.filter(program=program).first()
        self.assertIn("-R-", tracking.tracking_number)

    def test_quantity_capped_at_available_stock(self):
        from eveuniverse.models import EveType

        program = PublicReverseProgramFactory()
        eve_type = EveType.objects.filter(published=True).first()
        HangarStockFactory(program=program, eve_type=eve_type, quantity=5)
        ItemPricesFactory(eve_type=eve_type)

        payload = [{"type_id": eve_type.id, "quantity": 100}]
        response = self.client.post(
            reverse(
                "buybackprogram:reverse_program_calculate",
                kwargs={"program_pk": program.pk},
            ),
            data={"selected_items": json.dumps(payload)},
        )
        self.assertNotEqual(response.status_code, 302)
        buyback_data = response.context.get("buyback_data", [])
        if buyback_data:
            matched = [
                item for item in buyback_data if item.get("type_id") == eve_type.id
            ]
            if matched:
                self.assertLessEqual(matched[0]["quantity"], 5)

    def test_item_not_in_stock_gets_note(self):
        from eveuniverse.models import EveType

        program = PublicReverseProgramFactory()
        eve_type = EveType.objects.filter(published=True).first()
        ItemPricesFactory(eve_type=eve_type)

        payload = [{"type_id": eve_type.id, "quantity": 1}]
        response = self.client.post(
            reverse(
                "buybackprogram:reverse_program_calculate",
                kwargs={"program_pk": program.pk},
            ),
            data={"selected_items": json.dumps(payload)},
        )
        self.assertNotEqual(response.status_code, 302)
        self.assertIsNone(ReverseTracking.objects.filter(program=program).first())

    def test_empty_selection_creates_no_tracking(self):
        program = PublicReverseProgramFactory()
        response = self.client.post(
            reverse(
                "buybackprogram:reverse_program_calculate",
                kwargs={"program_pk": program.pk},
            ),
            data={"selected_items": json.dumps([])},
        )
        self.assertNotEqual(response.status_code, 302)
        self.assertEqual(ReverseTracking.objects.filter(program=program).count(), 0)


# ---------------------------------------------------------------------------
# Reverse buyback management view access
# ---------------------------------------------------------------------------


class TestReverseProgramManagement(TestCase):
    """Management views require manage_programs permission."""

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

    def setUp(self):
        self.manager = UserProjectManagerFactory()
        self.client = Client(raise_request_exception=False)
        self.client.force_login(self.manager)

    def test_manager_can_access_reverse_program_add(self):
        response = self.client.get(reverse("buybackprogram:reverse_program_add"))
        self.assertNotEqual(response.status_code, 302)

    def test_manager_can_edit_own_reverse_program(self):
        owner = OwnerFactory(user=self.manager)
        program = ReverseProgramFactory(owner=owner)
        response = self.client.get(
            reverse(
                "buybackprogram:reverse_program_edit",
                kwargs={"program_pk": program.pk},
            )
        )
        self.assertNotEqual(response.status_code, 302)

    def test_manager_cannot_edit_other_managers_program(self):
        other_program = ReverseProgramFactory()
        response = self.client.get(
            reverse(
                "buybackprogram:reverse_program_edit",
                kwargs={"program_pk": other_program.pk},
            )
        )
        self.assertNotEqual(response.status_code, 200)

    def test_manager_can_delete_own_reverse_program(self):
        owner = OwnerFactory(user=self.manager)
        program = ReverseProgramFactory(owner=owner)
        response = self.client.post(
            reverse(
                "buybackprogram:reverse_program_remove",
                kwargs={"program_pk": program.pk},
            )
        )
        self.assertRedirects(
            response,
            reverse("buybackprogram:reverse_index"),
            fetch_redirect_response=False,
        )
        from buybackprogram.models import ReverseProgram

        self.assertFalse(ReverseProgram.objects.filter(pk=program.pk).exists())
