from django.test import TestCase

from shop.models import FittedListing, Shop, ShopSource
from shop.models.base import FITTED
from shop.sections.fitted.stock import _strip_marker, reconcile_fitted_listings
from shop.tests._corptools import (
    make_asset,
    make_owner_character,
    make_ship_sde_type,
    make_ship_type,
)


class StripMarkerTests(TestCase):
    def test_strips_marker_case_insensitive_and_trims(self):
        self.assertEqual(_strip_marker("Nyx [SALE]", "[sale]"), "Nyx")
        self.assertEqual(_strip_marker("[SALE] Nyx", "[SALE]"), "Nyx")

    def test_empty_marker_returns_trimmed_name(self):
        self.assertEqual(_strip_marker("  Nyx  ", ""), "Nyx")

    def test_none_name_returns_empty(self):
        self.assertEqual(_strip_marker(None, "[SALE]"), "")

    def test_marker_absent_returns_trimmed_name(self):
        self.assertEqual(_strip_marker("Nyx", "[SALE]"), "Nyx")

    def test_mid_name_marker_collapses_gap(self):
        # an embedded marker must not leave a double space in the label
        self.assertEqual(_strip_marker("Nyx [SALE] alpha", "[SALE]"), "Nyx alpha")

    def test_strips_every_occurrence(self):
        # a marker appearing twice must be fully removed, not just the first
        self.assertEqual(
            _strip_marker("[SALE] Vindicator [SALE]", "[SALE]"), "Vindicator"
        )

    def test_filter_whitespace_is_normalized(self):
        # a filter with stray whitespace still strips cleanly (matches detection which
        # also .strip()s the filter) so no marker residue leaks to the storefront
        self.assertEqual(_strip_marker("[SALE] Vindi", " [SALE] "), "Vindi")


class ReconcileTests(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,
            name_filter="[SALE]",
            strip_marker=True,
        )
        # hull type in BOTH eve_sde (category detection) and eveuniverse (stored type)
        make_ship_sde_type(type_id=23773, category_id=6, group_id=30)
        make_ship_type(type_id=23773, name="Ragnarok")
        make_ship_sde_type(type_id=3057, category_id=7, group_id=55)  # a module

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

    def _module_in(self, ship_item_id, item_id):
        from eve_sde.models import ItemType

        return make_asset(
            self.char,
            type_id=3057,
            item_id=item_id,
            location_id=ship_item_id,
            location_flag="HiSlot0",
            type_name=ItemType.objects.get(id=3057),
        )

    def test_autolists_marked_ship_with_stripped_name(self):
        self._ship(9001, "Doomsday [SALE]")
        self._module_in(9001, 8001)
        reconcile_fitted_listings(self.shop)
        listing = FittedListing.objects.get(shop=self.shop, item_id=9001)
        self.assertEqual(listing.eve_type_id, 23773)
        self.assertEqual(listing.ship_name, "Doomsday")  # marker stripped
        self.assertEqual(listing.fit_json["slots"][0]["type_id"], 3057)

    def test_ignores_unmarked_ship(self):
        self._ship(9002, "My ratting Rag")
        reconcile_fitted_listings(self.shop)
        self.assertFalse(FittedListing.objects.filter(item_id=9002).exists())

    def test_strip_marker_false_keeps_full_name(self):
        self.source.strip_marker = False
        self.source.save()
        self._ship(9003, "Doomsday [SALE]")
        reconcile_fitted_listings(self.shop)
        self.assertEqual(
            FittedListing.objects.get(item_id=9003).ship_name, "Doomsday [SALE]"
        )

    def test_recompute_resnapshots_and_preserves_featured_and_price(self):
        self._ship(9004, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        listing = FittedListing.objects.get(item_id=9004)
        listing.featured = True
        listing.price = 90_000_000_000
        listing.save()
        # add a module, reconcile again -> fit re-snapshots, featured/price kept
        self._module_in(9004, 8004)
        reconcile_fitted_listings(self.shop)
        listing.refresh_from_db()
        self.assertTrue(listing.featured)
        self.assertEqual(listing.price, 90_000_000_000)
        self.assertEqual(len(listing.fit_json["slots"]), 1)

    def test_drops_vanished_ship_after_grace(self):
        ship = self._ship(9005, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        self.assertTrue(FittedListing.objects.filter(item_id=9005).exists())
        ship.delete()  # ship sold/moved
        reconcile_fitted_listings(self.shop)  # 1st miss — grace, still listed
        self.assertTrue(FittedListing.objects.filter(item_id=9005).exists())
        reconcile_fitted_listings(self.shop)  # 2nd miss — confirmed gone, removed
        self.assertFalse(FittedListing.objects.filter(item_id=9005).exists())

    def test_grace_survives_a_single_empty_read(self):
        # one transient miss (e.g. corptools mid-refresh) must NOT delete the listing
        ship = self._ship(9013, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        listing = FittedListing.objects.get(item_id=9013)
        listing.price = 90_000_000_000
        listing.save()
        ship.delete()
        reconcile_fitted_listings(self.shop)  # single miss
        listing.refresh_from_db()  # still here, price intact
        self.assertEqual(listing.miss_count, 1)
        self.assertEqual(listing.price, 90_000_000_000)

    def test_returning_ship_resets_grace(self):
        # a ship that reappears before the grace runs out keeps its listing + resets
        ship = self._ship(9014, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        ship.delete()
        reconcile_fitted_listings(self.shop)  # miss 1
        self.assertEqual(FittedListing.objects.get(item_id=9014).miss_count, 1)
        self._ship(9014, "Rag [SALE]")  # back before miss 2
        reconcile_fitted_listings(self.shop)
        listing = FittedListing.objects.get(item_id=9014)
        self.assertEqual(listing.miss_count, 0)
        self.assertEqual(listing.stock, 1)

    def test_no_sources_cleans_up_immediately(self):
        # removing the stockroom (no fitted sources left) is unambiguous -> no grace
        self._ship(9015, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        self.assertTrue(FittedListing.objects.filter(item_id=9015).exists())
        self.source.delete()  # stockroom removed
        reconcile_fitted_listings(self.shop)
        self.assertFalse(FittedListing.objects.filter(item_id=9015).exists())

    def test_present_ship_has_stock_one(self):
        self._ship(9010, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        self.assertEqual(FittedListing.objects.get(item_id=9010).stock, 1)

    def test_persist_keeps_gone_ship_as_build_to_order(self):
        ship = self._ship(9011, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        listing = FittedListing.objects.get(item_id=9011)
        listing.persist = True
        listing.price = 90_000_000_000
        listing.save()
        ship.delete()  # ship leaves the stockroom
        reconcile_fitted_listings(self.shop)  # grace
        reconcile_fitted_listings(self.shop)  # confirmed gone
        listing.refresh_from_db()  # not deleted
        self.assertEqual(listing.stock, 0)  # out of stock -> build-to-order
        self.assertEqual(listing.price, 90_000_000_000)  # price preserved

    def test_returning_ship_restores_stock(self):
        ship = self._ship(9012, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        FittedListing.objects.filter(item_id=9012).update(persist=True)
        ship.delete()
        reconcile_fitted_listings(self.shop)  # grace
        reconcile_fitted_listings(self.shop)  # confirmed gone -> stock 0
        self.assertEqual(FittedListing.objects.get(item_id=9012).stock, 0)
        self._ship(9012, "Rag [SALE]")  # ship comes back
        reconcile_fitted_listings(self.shop)
        self.assertEqual(FittedListing.objects.get(item_id=9012).stock, 1)

    def test_transient_esi_failure_preserves_existing_listing(self):
        # ship still physically present, but its hull type fails to resolve this
        # cycle (ESI blip). The existing listing + its price/featured must survive.
        self._ship(9006, "Rag [SALE]")
        reconcile_fitted_listings(self.shop)
        listing = FittedListing.objects.get(item_id=9006)
        listing.price = 90_000_000_000
        listing.featured = True
        listing.save()
        from unittest import mock

        with mock.patch(
            "eveuniverse.models.EveType.objects.get_or_create_esi",
            side_effect=OSError("ESI down"),
        ):
            reconcile_fitted_listings(self.shop)
        listing.refresh_from_db()  # DoesNotExist here would mean it was wrongly deleted
        self.assertEqual(listing.price, 90_000_000_000)
        self.assertTrue(listing.featured)
