# Django
from django.test import TestCase

# Alliance Auth (External Libs)
from eve_sde.models import ItemCategory, ItemGroup, ItemType

from incursions.api.helpers.fittings import FitError, FittingParser


class TestFittingParserBase(TestCase):
    """Hand-built catalogue shared by DNA / EFT / analysis tests."""

    def setUp(self) -> None:
        ship_category = ItemCategory.objects.create(id=6, name="Ship", name_en="Ship", published=True)
        module_category = ItemCategory.objects.create(id=7, name="Module", name_en="Module", published=True)
        charge_category = ItemCategory.objects.create(id=8, name="Charge", name_en="Charge", published=True)
        drone_category = ItemCategory.objects.create(id=18, name="Drone", name_en="Drone", published=True)

        ship_group = ItemGroup.objects.create(id=600, name="Cruiser", name_en="Cruiser", category=ship_category, published=True)
        module_group = ItemGroup.objects.create(id=700, name="Shield Booster", name_en="Shield Booster", category=module_category, published=True)
        charge_group = ItemGroup.objects.create(id=800, name="Charge", name_en="Charge", category=charge_category, published=True)
        drone_group = ItemGroup.objects.create(id=1800, name="Combat Drone", name_en="Combat Drone", category=drone_category, published=True)

        self.ship = ItemType.objects.create(id=600, name="Sample Cruiser", name_en="Sample Cruiser", group=ship_group, published=True)
        self.unpublished_ship = ItemType.objects.create(id=601, name="Old Cruiser", name_en="Old Cruiser", group=ship_group, published=False)
        self.module = ItemType.objects.create(id=700, name="Sample Module", name_en="Sample Module", group=module_group, published=True)
        self.unpublished_module = ItemType.objects.create(id=701, name="Old Module", name_en="Old Module", group=module_group, published=False)
        self.charge = ItemType.objects.create(id=800, name="Sample Charge", name_en="Sample Charge", group=charge_group, published=True)
        self.drone = ItemType.objects.create(id=1800, name="Sample Drone", name_en="Sample Drone", group=drone_group, published=True)


class TestFromDna(TestFittingParserBase):
    def test_module_and_cargo_split_via_underscore_suffix(self) -> None:
        dna = f"{self.ship.id}:{self.module.id};2:{self.charge.id}_;3:"

        parsed = FittingParser.from_dna(dna)

        self.assertEqual(parsed.ship, self.ship.id)
        self.assertEqual(parsed.modules, {self.module.id: 2})
        self.assertEqual(parsed.cargo, {self.charge.id: 3})

    def test_cargo_via_always_cargo_category(self) -> None:
        # No underscore suffix - the charge category is in ALWAYS_CARGO_CATEGORIES.
        dna = f"{self.ship.id}:{self.charge.id};4:"

        parsed = FittingParser.from_dna(dna)

        self.assertEqual(parsed.cargo, {self.charge.id: 4})
        self.assertEqual(parsed.modules, {})

    def test_count_defaults_to_one_and_sums_repeated_ids(self) -> None:
        dna = f"{self.ship.id}:{self.module.id}:{self.module.id};2:"

        parsed = FittingParser.from_dna(dna)

        self.assertEqual(parsed.modules, {self.module.id: 3})

    def test_empty_string_raises_fit_error(self) -> None:
        with self.assertRaises(FitError):
            FittingParser.from_dna("")

    def test_non_numeric_ship_raises_fit_error(self) -> None:
        with self.assertRaises(FitError):
            FittingParser.from_dna("notanumber:")

    def test_non_numeric_count_raises_fit_error(self) -> None:
        with self.assertRaises(FitError):
            FittingParser.from_dna(f"{self.ship.id}:{self.module.id};notanumber:")

    def test_non_numeric_module_type_id_raises_fit_error(self) -> None:
        with self.assertRaises(FitError):
            FittingParser.from_dna(f"{self.ship.id}:notanumber;1:")

    def test_non_numeric_cargo_type_id_raises_fit_error(self) -> None:
        with self.assertRaises(FitError):
            FittingParser.from_dna(f"{self.ship.id}:notanumber_;1:")

    def test_too_many_pieces_raises_fit_error(self) -> None:
        # Underscore-suffixed cargo entries never hit the DB, keeping this fast.
        dna = f"{self.ship.id}:" + ":".join(["1_"] * 1001)

        with self.assertRaises(FitError):
            FittingParser.from_dna(dna)

    def test_empty_pieces_are_skipped(self) -> None:
        dna = f"{self.ship.id}::::{self.module.id};1:"

        parsed = FittingParser.from_dna(dna)

        self.assertEqual(parsed.modules, {self.module.id: 1})


class TestToDna(TestFittingParserBase):
    def test_round_trip_preserves_modules_and_cargo(self) -> None:
        original = FittingParser(
            ship=self.ship.id,
            modules={self.module.id: 2},
            cargo={self.charge.id: 3},
        )

        dna = original.to_dna()
        round_tripped = FittingParser.from_dna(dna)

        self.assertEqual(round_tripped.ship, self.ship.id)
        self.assertEqual(round_tripped.modules, {self.module.id: 2})
        self.assertEqual(round_tripped.cargo, {self.charge.id: 3})

    def test_underscore_only_appended_for_non_always_cargo_items(self) -> None:
        # self.module (category Module) is not in ALWAYS_CARGO_CATEGORIES, but here
        # it's deliberately placed in the cargo dict to check the underscore branch.
        fp = FittingParser(ship=self.ship.id, modules={}, cargo={self.module.id: 1, self.charge.id: 1})

        dna = fp.to_dna()

        self.assertIn(f"{self.module.id}_;1:", dna)
        self.assertIn(f"{self.charge.id};1:", dna)
        self.assertNotIn(f"{self.charge.id}_;1:", dna)


class TestFromEft(TestFittingParserBase):
    def test_header_parses_ship_name(self) -> None:
        eft = f"[{self.ship.name_en}, My Fit]\n"

        parsed = FittingParser.from_eft(eft)

        self.assertEqual(parsed.ship, self.ship.id)

    def test_multiple_headers_raises_fit_error(self) -> None:
        eft = f"[{self.ship.name_en}, Fit One]\n[{self.ship.name_en}, Fit Two]\n"

        with self.assertRaises(FitError):
            FittingParser.from_eft(eft)

    def test_missing_ship_name_raises_fit_error(self) -> None:
        with self.assertRaises(FitError):
            FittingParser.from_eft("[Ship Name,]\n")

    def test_no_header_raises_fit_error(self) -> None:
        with self.assertRaises(FitError):
            FittingParser.from_eft(f"{self.module.name_en}\n")

    def test_empty_module_lines_are_skipped(self) -> None:
        eft = f"[{self.ship.name_en}, My Fit]\n[Empty High slot]\n{self.module.name_en}\n"

        parsed = FittingParser.from_eft(eft)

        self.assertEqual(parsed.modules, {self.module.id: 1})

    def test_charge_stripped_after_comma(self) -> None:
        eft = f"[{self.ship.name_en}, My Fit]\n{self.module.name_en}, Some Script\n"

        parsed = FittingParser.from_eft(eft)

        self.assertEqual(parsed.modules, {self.module.id: 1})

    def test_stacked_non_drone_module_is_treated_as_cargo(self) -> None:
        # Within a slot section, a stacked ("x5") count on a non-Drone, non-always-cargo
        # item routes it into cargo rather than modules (the "probably ammo" heuristic).
        eft = f"[{self.ship.name_en}, My Fit]\n{self.module.name_en} x5\n"

        parsed = FittingParser.from_eft(eft)

        self.assertEqual(parsed.cargo, {self.module.id: 5})
        self.assertEqual(parsed.modules, {})

    def test_section_seven_or_later_is_treated_as_cargo(self) -> None:
        eft = f"[{self.ship.name_en}, My Fit]\n" + "\n" * 7 + f"{self.module.name_en}\n"

        parsed = FittingParser.from_eft(eft)

        self.assertEqual(parsed.cargo, {self.module.id: 1})
        self.assertEqual(parsed.modules, {})

    def test_unpublished_ship_used_as_fallback_lookup(self) -> None:
        eft = f"[{self.unpublished_ship.name_en}, My Fit]\n"

        parsed = FittingParser.from_eft(eft)

        self.assertEqual(parsed.ship, self.unpublished_ship.id)

    def test_unpublished_module_used_as_fallback_lookup(self) -> None:
        eft = f"[{self.ship.name_en}, My Fit]\n{self.unpublished_module.name_en}\n"

        parsed = FittingParser.from_eft(eft)

        self.assertEqual(parsed.modules, {self.unpublished_module.id: 1})

    def test_drone_category_carve_out_keeps_stacked_drones_out_of_cargo(self) -> None:
        eft = f"[{self.ship.name_en}, My Fit]\n{self.drone.name_en} x5\n"

        parsed = FittingParser.from_eft(eft)

        self.assertEqual(parsed.modules, {self.drone.id: 5})
        self.assertEqual(parsed.cargo, {})


class TestToAnalysis(TestFittingParserBase):
    def test_reports_missing_modules(self) -> None:
        doctrine = FittingParser(ship=self.ship.id, modules={self.module.id: 2}, cargo={})
        actual = FittingParser(ship=self.ship.id, modules={}, cargo={})

        analysis = actual.to_analysis(doctrine=doctrine, name="Test Doctrine")

        self.assertEqual(analysis["name"], "Test Doctrine")
        self.assertEqual(analysis["missing"], {self.module.id: 2})
        self.assertEqual(analysis["extra"], {})

    def test_reports_extra_modules(self) -> None:
        doctrine = FittingParser(ship=self.ship.id, modules={}, cargo={})
        actual = FittingParser(ship=self.ship.id, modules={self.module.id: 1}, cargo={})

        analysis = actual.to_analysis(doctrine=doctrine)

        self.assertEqual(analysis["extra"], {self.module.id: 1})
        self.assertEqual(analysis["missing"], {})

    def test_reports_missing_cargo(self) -> None:
        doctrine = FittingParser(ship=self.ship.id, modules={}, cargo={self.charge.id: 10})
        actual = FittingParser(ship=self.ship.id, modules={}, cargo={})

        analysis = actual.to_analysis(doctrine=doctrine)

        self.assertEqual(analysis["cargo_missing"], {self.charge.id: 10})

    def test_empty_diff_when_fits_match(self) -> None:
        doctrine = FittingParser(ship=self.ship.id, modules={self.module.id: 1}, cargo={self.charge.id: 5})
        actual = FittingParser(ship=self.ship.id, modules={self.module.id: 1}, cargo={self.charge.id: 5})

        analysis = actual.to_analysis(doctrine=doctrine)

        self.assertEqual(analysis["missing"], {})
        self.assertEqual(analysis["extra"], {})
        self.assertEqual(analysis["cargo_missing"], {})
        self.assertEqual(analysis["name"], "Unnamed Analysis")
