from django.test import TestCase

from shop.models import Shop, ShopSource
from shop.models.base import FITTED
from shop.sections.fitted.reconstruct import (
    _categories_for,
    fit_display_groups,
    fit_to_eft,
    fitted_ships_in_source,
    reconstruct_fit,
    slot_section,
)
from shop.tests._corptools import make_asset, make_owner_character, make_ship_sde_type


class SlotSectionTests(TestCase):
    def test_maps_slot_flags_to_fitting_sections(self):
        self.assertEqual(slot_section("HiSlot0"), "high")
        self.assertEqual(slot_section("HiSlot7"), "high")
        self.assertEqual(slot_section("MedSlot3"), "mid")
        self.assertEqual(slot_section("LoSlot1"), "low")
        self.assertEqual(slot_section("RigSlot2"), "rig")
        self.assertEqual(slot_section("SubSystemSlot0"), "subsystem")

    def test_non_slot_flags_return_none(self):
        for flag in [
            "DroneBay",
            "Cargo",
            "ShipHangar",
            "FighterBay",
            "SpecializedFuelBay",
            "Hangar",
            "FleetHangar",
        ]:
            self.assertIsNone(slot_section(flag))


class CategoriesForTests(TestCase):
    def test_maps_type_ids_to_category_ids(self):
        make_ship_sde_type(type_id=671, category_id=6, group_id=30)  # ship
        make_ship_sde_type(type_id=12608, category_id=8, group_id=85)  # charge
        make_ship_sde_type(type_id=3057, category_id=7, group_id=55)  # module
        result = _categories_for([671, 12608, 3057])
        self.assertEqual(result, {671: 6, 12608: 8, 3057: 7})

    def test_unknown_ids_absent_from_map(self):
        self.assertEqual(_categories_for([999999]), {})


class FittedShipsInSourceTests(TestCase):
    STATION = 60003760

    def setUp(self):
        self.user, self.char = make_owner_character()
        self.shop = Shop.objects.create(slug="s", name="S", created_by=self.user)
        self.source = ShopSource.objects.create(
            shop=self.shop,
            section=FITTED,
            character_audit=self.char,
            location_id=self.STATION,
        )
        # eve_sde categories: ship=6, module=7
        make_ship_sde_type(type_id=671, category_id=6, group_id=30)  # Erebus (ship)
        make_ship_sde_type(type_id=578, category_id=6, group_id=25)  # another ship
        make_ship_sde_type(type_id=3057, category_id=7, group_id=55)  # a module

    def _ship(self, item_id, type_id=671, name=None):
        return make_asset(
            self.char,
            type_id=type_id,
            item_id=item_id,
            location_id=self.STATION,
            name=name,
        )

    def test_detects_singleton_ship(self):
        ship = self._ship(item_id=1001, name="My Titan")
        found = fitted_ships_in_source(self.source)
        self.assertEqual([a.item_id for a in found], [ship.item_id])

    def test_ignores_packaged_ship(self):
        # packaged ships stack: singleton False
        from corptools.models import CharacterAsset

        make_asset(
            self.char, type_id=671, item_id=1002, location_id=self.STATION, quantity=3
        )
        CharacterAsset.objects.filter(item_id=1002).update(singleton=False)
        self.assertEqual(fitted_ships_in_source(self.source), [])

    def test_ignores_non_ship_singleton(self):
        # a singleton module sitting loose in the hangar is not a ship
        self._ship(item_id=1003, type_id=3057)  # module category
        self.assertEqual(fitted_ships_in_source(self.source), [])

    def test_name_filter_includes_only_marked(self):
        self._ship(item_id=1004, name="Nyx [SALE]")
        self._ship(item_id=1005, name="Nyx ratting")
        found = fitted_ships_in_source(self.source, name_filter="[SALE]")
        self.assertEqual([a.item_id for a in found], [1004])

    def test_name_filter_is_case_insensitive(self):
        self._ship(item_id=1006, name="Nyx [sale]")
        found = fitted_ships_in_source(self.source, name_filter="[SALE]")
        self.assertEqual([a.item_id for a in found], [1006])

    def test_empty_filter_lists_all_ships(self):
        self._ship(item_id=1007, name="A")
        self._ship(item_id=1008, name=None)
        found = fitted_ships_in_source(self.source, name_filter="")
        self.assertEqual({a.item_id for a in found}, {1007, 1008})


class ReconstructSlotsTests(TestCase):
    STATION = 60003760

    def setUp(self):
        self.user, self.char = make_owner_character()
        make_ship_sde_type(type_id=671, category_id=6, group_id=30)  # hull
        make_ship_sde_type(type_id=3057, category_id=7, group_id=55)  # gun (module)
        make_ship_sde_type(type_id=12608, category_id=8, group_id=85)  # charge
        make_ship_sde_type(type_id=2048, category_id=7, group_id=60)  # damage control
        self.ship = make_asset(
            self.char,
            type_id=671,
            item_id=5000,
            location_id=self.STATION,
            name="Doomsday Express",
            type_name=None,
        )

    def _installed(self, type_id, flag, item_id, qty=1):
        # give each installed asset a resolvable eve_sde name via type_name
        from eve_sde.models import ItemType

        return make_asset(
            self.char,
            type_id=type_id,
            item_id=item_id,
            location_id=self.ship.item_id,
            location_flag=flag,
            quantity=qty,
            type_name=ItemType.objects.get(id=type_id),
        )

    def test_groups_modules_by_slot_ordered(self):
        self._installed(2048, "LoSlot0", 6001)  # low
        self._installed(3057, "HiSlot0", 6002)  # high
        fit = reconstruct_fit(self.ship)
        # display order: high before low
        self.assertEqual([s["flag"] for s in fit["slots"]], ["HiSlot0", "LoSlot0"])
        self.assertEqual(fit["slots"][0]["type_id"], 3057)
        self.assertEqual(fit["slots"][0]["name"], "type3057")
        self.assertIsNone(fit["slots"][0]["charge"])

    def test_pairs_charge_to_module_in_same_slot(self):
        self._installed(3057, "HiSlot0", 6003)  # gun
        self._installed(12608, "HiSlot0", 6004)  # charge in same slot
        fit = reconstruct_fit(self.ship)
        self.assertEqual(len(fit["slots"]), 1)
        self.assertEqual(fit["slots"][0]["type_id"], 3057)
        self.assertEqual(
            fit["slots"][0]["charge"], {"type_id": 12608, "name": "type12608"}
        )

    def test_empty_slots_absent(self):
        self._installed(3057, "HiSlot0", 6005)
        fit = reconstruct_fit(self.ship)
        self.assertEqual(len(fit["slots"]), 1)  # only the filled slot


class ReconstructBaysTests(TestCase):
    STATION = 60003760

    def setUp(self):
        self.user, self.char = make_owner_character()
        make_ship_sde_type(type_id=671, category_id=6, group_id=30)  # hull
        make_ship_sde_type(type_id=2456, category_id=18, group_id=100)  # drone
        make_ship_sde_type(type_id=12608, category_id=8, group_id=85)  # charge (cargo)
        make_ship_sde_type(type_id=28352, category_id=6, group_id=883)  # Rorqual (SMB)
        make_ship_sde_type(type_id=4051, category_id=17, group_id=1136)  # fuel block
        self.ship = make_asset(
            self.char,
            type_id=671,
            item_id=7000,
            location_id=self.STATION,
            name="Loaded Nyx",
        )

    def _installed(self, type_id, flag, item_id, qty=1):
        from eve_sde.models import ItemType

        return make_asset(
            self.char,
            type_id=type_id,
            item_id=item_id,
            location_id=self.ship.item_id,
            location_flag=flag,
            quantity=qty,
            type_name=ItemType.objects.get(id=type_id),
        )

    def test_drones_aggregate_quantity(self):
        self._installed(2456, "DroneBay", 7101, qty=5)
        self._installed(2456, "DroneBay", 7102, qty=3)  # second stack, same type
        fit = reconstruct_fit(self.ship)
        self.assertEqual(
            fit["drones"], [{"type_id": 2456, "name": "type2456", "qty": 8}]
        )

    def test_cargo_bucket(self):
        self._installed(12608, "Cargo", 7103, qty=100)
        fit = reconstruct_fit(self.ship)
        self.assertEqual(
            fit["cargo"], [{"type_id": 12608, "name": "type12608", "qty": 100}]
        )

    def test_smb_ships_go_to_manifest_not_recursed(self):
        self._installed(28352, "ShipHangar", 7104, qty=1)
        self._installed(28352, "ShipHangar", 7105, qty=1)  # two Rorquals
        fit = reconstruct_fit(self.ship)
        self.assertEqual(
            fit["manifest"],
            [
                {"flag": "ShipHangar", "type_id": 28352, "name": "type28352", "qty": 2},
            ],
        )
        # not recursed: the Rorquals do not appear as slots/drones
        self.assertEqual(fit["slots"], [])
        self.assertEqual(fit["drones"], [])

    def test_fuel_bay_goes_to_manifest(self):
        self._installed(4051, "SpecializedFuelBay", 7106, qty=25000)
        fit = reconstruct_fit(self.ship)
        self.assertEqual(
            fit["manifest"],
            [
                {
                    "flag": "SpecializedFuelBay",
                    "type_id": 4051,
                    "name": "type4051",
                    "qty": 25000,
                },
            ],
        )

    def test_charge_in_slot_without_module_falls_back_to_cargo(self):
        # a charge (cat 8) alone in a slot flag, no module in that slot -> cargo
        self._installed(12608, "HiSlot0", 7107, qty=1)
        fit = reconstruct_fit(self.ship)
        self.assertEqual(fit["slots"], [])
        self.assertEqual(
            fit["cargo"], [{"type_id": 12608, "name": "type12608", "qty": 1}]
        )


class EftSerializerTests(TestCase):
    def test_serializes_slots_charges_drones_cargo(self):
        fit = {
            "slots": [
                {
                    "flag": "LoSlot0",
                    "type_id": 2048,
                    "name": "Damage Control II",
                    "charge": None,
                },
                {
                    "flag": "HiSlot0",
                    "type_id": 3057,
                    "name": "425mm Railgun II",
                    "charge": {"type_id": 12608, "name": "Antimatter Charge L"},
                },
            ],
            "drones": [{"type_id": 2456, "name": "Ogre II", "qty": 5}],
            "cargo": [{"type_id": 12608, "name": "Antimatter Charge L", "qty": 100}],
            "manifest": [
                {"flag": "ShipHangar", "type_id": 28352, "name": "Rorqual", "qty": 2}
            ],
        }
        eft = fit_to_eft("Nyx", "Doomsday Express", fit)
        self.assertEqual(
            eft,
            (
                "[Nyx, Doomsday Express]\n"
                "Damage Control II\n"  # low first (EFT order)
                "\n"
                "425mm Railgun II, Antimatter Charge L\n"  # high, with charge
                "\n\n"
                "Ogre II x5\n"
                "\n"
                "Antimatter Charge L x100"
            ),
        )

    def test_manifest_is_excluded_from_eft(self):
        fit = {
            "slots": [],
            "drones": [],
            "cargo": [],
            "manifest": [
                {"flag": "ShipHangar", "type_id": 28352, "name": "Rorqual", "qty": 2}
            ],
        }
        eft = fit_to_eft("Nyx", "Empty", fit)
        self.assertEqual(eft, "[Nyx, Empty]")
        self.assertNotIn("Rorqual", eft)

    def test_only_present_sections_appear(self):
        fit = {
            "slots": [
                {
                    "flag": "MedSlot0",
                    "type_id": 999,
                    "name": "Large Shield Extender II",
                    "charge": None,
                }
            ],
            "drones": [],
            "cargo": [],
            "manifest": [],
        }
        eft = fit_to_eft("Ferox", "Shields", fit)
        self.assertEqual(eft, "[Ferox, Shields]\nLarge Shield Extender II")

    def test_comma_in_fit_name_is_stripped(self):
        # user-supplied ship name with a comma must not break the [Hull, Name] header
        fit = {"slots": [], "drones": [], "cargo": [], "manifest": []}
        eft = fit_to_eft("Nyx", "Nyx, backup", fit)
        self.assertEqual(eft, "[Nyx, Nyx backup]")


class FitDisplayGroupsTests(TestCase):
    def test_groups_fitting_in_display_order_with_counts(self):
        fit = {
            "slots": [
                {
                    "flag": "LoSlot0",
                    "type_id": 2048,
                    "name": "Damage Control II",
                    "charge": None,
                },
                {"flag": "HiSlot1", "type_id": 3057, "name": "Gun", "charge": None},
                {
                    "flag": "HiSlot0",
                    "type_id": 3057,
                    "name": "Gun",
                    "charge": {"type_id": 12608, "name": "Antimatter L"},
                },
            ],
            "drones": [{"type_id": 2456, "name": "Ogre II", "qty": 5}],
            "cargo": [],
            "manifest": [],
        }
        groups = fit_display_groups(fit)
        # High before Low (display order); high slots sorted by index
        self.assertEqual([g["label"] for g in groups["fitting"]], ["High", "Low"])
        high = groups["fitting"][0]
        self.assertEqual(high["count"], 2)
        self.assertEqual([i["flag"] for i in high["items"]], ["HiSlot0", "HiSlot1"])
        self.assertEqual(
            groups["drones"], [{"type_id": 2456, "name": "Ogre II", "qty": 5}]
        )

    def test_empty_fitting_groups_omitted(self):
        fit = {
            "slots": [
                {"flag": "MedSlot0", "type_id": 9, "name": "SE II", "charge": None}
            ],
            "drones": [],
            "cargo": [],
            "manifest": [],
        }
        groups = fit_display_groups(fit)
        self.assertEqual([g["label"] for g in groups["fitting"]], ["Mid"])

    def test_manifest_gets_location_label(self):
        fit = {
            "slots": [],
            "drones": [],
            "cargo": [],
            "manifest": [
                {"flag": "ShipHangar", "type_id": 28352, "name": "Rorqual", "qty": 2},
                {
                    "flag": "SpecializedFuelBay",
                    "type_id": 4051,
                    "name": "Fuel",
                    "qty": 9,
                },
            ],
        }
        groups = fit_display_groups(fit)
        labels = {m["location"] for m in groups["manifest"]}
        self.assertEqual(labels, {"Ship Maintenance Bay", "Fuel Bay"})

    def test_handles_empty_fit(self):
        groups = fit_display_groups({})
        self.assertEqual(
            groups, {"fitting": [], "drones": [], "cargo": [], "manifest": []}
        )

    def test_handles_none_valued_keys(self):
        # a partial/corrupt fit_json with explicit None must not crash
        fit = {"slots": None, "drones": None, "cargo": None, "manifest": None}
        groups = fit_display_groups(fit)
        self.assertEqual(
            groups, {"fitting": [], "drones": [], "cargo": [], "manifest": []}
        )


class EftEmptyFitTests(TestCase):
    def test_eft_of_empty_dict_is_header_only(self):
        # fit_json may be {} for a listing not yet reconstructed — must not KeyError
        self.assertEqual(fit_to_eft("Rifter", "Bare", {}), "[Rifter, Bare]")

    def test_eft_tolerates_none_valued_keys(self):
        fit = {"slots": None, "drones": None, "cargo": None, "manifest": None}
        self.assertEqual(fit_to_eft("Rifter", "Bare", fit), "[Rifter, Bare]")
