# Standard Library
from datetime import datetime, timedelta
from types import SimpleNamespace
from unittest.mock import Mock, patch

# Third Party
from discord import Embed

# Django
from django.test import SimpleTestCase

from incursions import helpers


class TestHelperFunctions(SimpleTestCase):
    def test_constellation_meta_returns_fallback_when_missing(self) -> None:
        with patch("incursions.helpers.logger.warning") as mock_warning:
            result = helpers._constellation_meta("Does Not Exist")

        self.assertEqual(
            result,
            {
                "Headquarter": "Unknown",
                "Assaults": [],
                "Vanguards": [],
            },
        )
        mock_warning.assert_called_once_with(
            "Constellation %s missing in static_data",
            "Does Not Exist",
        )

    def test_constellation_meta_returns_static_data_entry(self) -> None:
        expected = {
            "Headquarter": "Jita",
            "Assaults": ["A1"],
            "Vanguards": ["V1"],
        }

        with patch.dict("incursions.helpers.incursion_constellations", {"Alpha": expected}, clear=True):
            result = helpers._constellation_meta("Alpha")

        self.assertEqual(result, expected)

    def test_resolve_incursion_location_uses_fallback_lookups(self) -> None:
        incursion = Mock(constellation=None, staging_solar_system=None)
        fallback_constellation = SimpleNamespace(name="Fallback Constellation", region=None)
        fallback_region = SimpleNamespace(name="Fallback Region")
        fallback_system = SimpleNamespace(name="Fallback System", pk=30000142)

        with (
            patch("incursions.helpers.Constellation.objects.get", return_value=fallback_constellation) as mock_constellation_get,
            patch("incursions.helpers.Region.objects.get", return_value=fallback_region) as mock_region_get,
            patch("incursions.helpers.SolarSystem.objects.get", return_value=fallback_system) as mock_system_get,
        ):
            constellation, region, staging = helpers._resolve_incursion_location(incursion)

        self.assertIs(constellation, fallback_constellation)
        self.assertIs(region, fallback_region)
        self.assertIs(staging, fallback_system)
        mock_constellation_get.assert_called_once_with(
            pk=helpers.FALLBACK_CONSTELLATION_ID
        )
        mock_region_get.assert_called_once_with(pk=helpers.FALLBACK_REGION_ID)
        mock_system_get.assert_called_once_with(pk=helpers.FALLBACK_SYSTEM_ID)

    def test_embed_established_addendum_high_sec_adds_mommy_time_and_route_flags(self) -> None:
        established_time = datetime(2026, 3, 25, 12, 0, 0)
        incursion = Mock(
            established_timestamp=established_time,
            is_high_sec=True,
            is_low_sec=False,
            security_string="High Sec",
        )

        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)
        focus_staging = SimpleNamespace(name="Old HQ", pk=30000142)
        focus = SimpleNamespace(incursion=SimpleNamespace(staging_solar_system=focus_staging))

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.route_length", return_value=7),
            patch("incursions.helpers.Focus.get_solo", return_value=focus),
            patch("incursions.helpers.route_check_triglavian", return_value=True),
            patch("incursions.helpers.route_check_edencom", return_value=False),
        ):
            embed = helpers.embed_established_addendum(incursion)

        fields = {field.name: str(field.value) for field in embed.fields}
        expected_mommy = int((established_time + timedelta(days=3)).timestamp())
        self.assertEqual(fields["Mommy Time"], f"<t:{expected_mommy}:R>")
        self.assertEqual(fields["Triglavian In Route"], "True")
        self.assertEqual(fields["Edencom In Route"], "False")

    def test_embed_mobilizing_includes_spawn_stats_when_timestamps_present(self) -> None:
        established_time = datetime(2026, 3, 20, 12, 0, 0)
        mobilizing_time = datetime(2026, 3, 22, 0, 0, 0)
        incursion = Mock(
            mobilizing_timestamp=mobilizing_time,
            established_timestamp=established_time,
            security_string="High Sec",
        )

        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_mobilizing(incursion)

        fields = {field.name: str(field.value) for field in embed.fields}
        self.assertIn("Max Spawn Stats", fields)
        self.assertIn("Uptime", fields["Max Spawn Stats"])
        self.assertIn("Unused", fields["Max Spawn Stats"])

    def test_embed_mobilizing_skips_spawn_stats_when_missing_timestamps(self) -> None:
        incursion = Mock(
            mobilizing_timestamp=None,
            established_timestamp=None,
            security_string="Low Sec",
        )

        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_mobilizing(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertNotIn("Max Spawn Stats", field_names)

    def test_embed_waitlist_state_sets_open_and_closed_variants(self) -> None:
        with patch("incursions.helpers.get_site_url", return_value="https://example.com"):
            open_embed = helpers.embed_waitlist_state(waitlist=Mock(), is_open=True)
            closed_embed = helpers.embed_waitlist_state(waitlist=Mock(), is_open=False)

        self.assertEqual(open_embed.title, "Waitlist Open")
        self.assertEqual(open_embed.description, "Waitlist Open")
        self.assertEqual(open_embed.url, "https://example.com/incursions/waitlist/")

        self.assertEqual(closed_embed.title, "Waitlist Closed")
        self.assertEqual(closed_embed.description, "Waitlist Closed")

    def test_resolve_incursion_location_uses_present_constellation_and_region(self) -> None:
        staging_system = SimpleNamespace(name="Staging", pk=30000001)
        region = SimpleNamespace(name="Region")
        constellation = SimpleNamespace(name="Constellation", region=region)
        incursion = Mock(constellation=constellation, staging_solar_system=staging_system)

        with (
            patch("incursions.helpers.Region.objects.get") as mock_region_get,
            patch("incursions.helpers.SolarSystem.objects.get") as mock_system_get,
        ):
            result_constellation, result_region, result_staging = helpers._resolve_incursion_location(incursion)

        self.assertIs(result_constellation, constellation)
        self.assertIs(result_region, region)
        self.assertIs(result_staging, staging_system)
        mock_region_get.assert_not_called()
        mock_system_get.assert_not_called()

    def test_resolve_incursion_location_falls_back_on_exceptions(self) -> None:
        class RaisingIncursion:
            @property
            def constellation(self):
                raise RuntimeError("boom")

            staging_solar_system = None

        fallback_constellation = SimpleNamespace(name="Fallback Constellation")  # no .region attribute
        fallback_region = SimpleNamespace(name="Fallback Region")
        fallback_system = SimpleNamespace(name="Fallback System")

        with (
            patch("incursions.helpers.Constellation.objects.first", return_value=fallback_constellation),
            patch("incursions.helpers.Region.objects.first", return_value=fallback_region),
            patch("incursions.helpers.SolarSystem.objects.get", side_effect=RuntimeError("boom")),
            patch("incursions.helpers.SolarSystem.objects.first", return_value=fallback_system),
        ):
            constellation, region, staging = helpers._resolve_incursion_location(RaisingIncursion())

        self.assertIs(constellation, fallback_constellation)
        self.assertIs(region, fallback_region)
        self.assertIs(staging, fallback_system)

    def test_embed_established_builds_expected_fields(self) -> None:
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)
        focus_staging = SimpleNamespace(name="Old HQ", pk=30000142)
        focus = SimpleNamespace(incursion=SimpleNamespace(staging_solar_system=focus_staging))
        incursion = Mock(security_string="High Sec")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.route_length", return_value=5),
            patch("incursions.helpers.Focus.get_solo", return_value=focus),
            patch.dict(
                "incursions.helpers.incursion_constellations",
                {"Constellation": {"Headquarter": "Jita", "Assaults": ["A"], "Vanguards": ["V"]}},
                clear=True,
            ),
        ):
            embed = helpers.embed_established(incursion)

        fields = {field.name: str(field.value) for field in embed.fields}
        self.assertEqual(fields["Region"], "Region")
        self.assertEqual(fields["Constellation"], "Constellation")
        self.assertEqual(fields["Headquarters"], "Jita")
        self.assertEqual(fields["Assault"], "A")
        self.assertEqual(fields["Vanguard"], "V")
        self.assertEqual(fields["Jumps from last HQ Old HQ"], "5")

    def test_embed_established_skips_jumps_field_without_focus_incursion(self) -> None:
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)
        focus = SimpleNamespace(incursion=None)
        incursion = Mock(security_string="Low Sec")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.Focus.get_solo", return_value=focus),
        ):
            embed = helpers.embed_established(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertFalse(any(name.startswith("Jumps from last HQ") for name in field_names))

    def test_embed_established_swallows_focus_lookup_exception(self) -> None:
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)
        incursion = Mock(security_string="High Sec")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.Focus.get_solo", side_effect=RuntimeError("no config")),
            patch("incursions.helpers.logger.debug") as mock_debug,
        ):
            embed = helpers.embed_established(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertFalse(any(name.startswith("Jumps from last HQ") for name in field_names))
        mock_debug.assert_any_call("Unable to compute jumps from last HQ", exc_info=True)

    def test_embed_established_addendum_uses_now_when_established_timestamp_missing(self) -> None:
        incursion = Mock(established_timestamp=None, is_high_sec=False, is_low_sec=True, security_string="Low Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.route_length", return_value=1),
            patch("incursions.helpers.Focus.get_solo", side_effect=RuntimeError("no config")),
            patch("incursions.helpers.logger.debug") as mock_debug,
        ):
            embed = helpers.embed_established_addendum(incursion)

        fields = {field.name: str(field.value) for field in embed.fields}
        self.assertIn("Mommy Time", fields)
        mock_debug.assert_any_call("Incursion %s missing established_timestamp", incursion.pk)

    def test_embed_established_addendum_null_sec_uses_two_day_delta(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, 0)
        incursion = Mock(established_timestamp=ts, is_high_sec=False, is_low_sec=False, security_string="Null Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.route_length", return_value=1),
            patch("incursions.helpers.Focus.get_solo", side_effect=RuntimeError("no config")),
        ):
            embed = helpers.embed_established_addendum(incursion)

        fields = {field.name: str(field.value) for field in embed.fields}
        expected_mommy = int((ts + timedelta(days=2)).timestamp())
        self.assertEqual(fields["Mommy Time"], f"<t:{expected_mommy}:R>")

    def test_embed_established_addendum_skips_route_hub_on_exception(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, 0)
        incursion = Mock(established_timestamp=ts, is_high_sec=True, is_low_sec=False, security_string="High Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.route_length", side_effect=RuntimeError("no route")),
            patch("incursions.helpers.Focus.get_solo", side_effect=RuntimeError("no config")),
            patch("incursions.helpers.logger.debug") as mock_debug,
        ):
            embed = helpers.embed_established_addendum(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertNotIn("HQ to Hek", field_names)
        self.assertGreaterEqual(mock_debug.call_count, 5)

    def test_embed_established_addendum_skips_trig_edencom_without_focus_staging(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, 0)
        incursion = Mock(established_timestamp=ts, is_high_sec=True, is_low_sec=False, security_string="High Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)
        focus = SimpleNamespace(incursion=SimpleNamespace(staging_solar_system=None))

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.route_length", return_value=1),
            patch("incursions.helpers.Focus.get_solo", return_value=focus),
        ):
            embed = helpers.embed_established_addendum(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertNotIn("Triglavian In Route", field_names)
        self.assertNotIn("Edencom In Route", field_names)

    def test_embed_established_addendum_swallows_trig_edencom_exception(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, 0)
        incursion = Mock(established_timestamp=ts, is_high_sec=True, is_low_sec=False, security_string="High Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")
        staging = SimpleNamespace(name="HQ", pk=30002510)
        focus_staging = SimpleNamespace(name="Old HQ", pk=30000142)
        focus = SimpleNamespace(incursion=SimpleNamespace(staging_solar_system=focus_staging))

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, staging)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.route_length", return_value=1),
            patch("incursions.helpers.Focus.get_solo", return_value=focus),
            patch("incursions.helpers.route_check_triglavian", side_effect=RuntimeError("boom")),
            patch("incursions.helpers.logger.debug") as mock_debug,
        ):
            embed = helpers.embed_established_addendum(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertNotIn("Triglavian In Route", field_names)
        mock_debug.assert_any_call("Failed trig/edencom route checks", exc_info=True)

    def test_embed_mobilizing_swallows_uptime_calc_exception(self) -> None:
        mobilizing_time = datetime(2026, 3, 22, 0, 0, 0)
        incursion = Mock(
            mobilizing_timestamp=mobilizing_time,
            established_timestamp="not-a-datetime",
            security_string="High Sec",
        )
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_mobilizing(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertNotIn("Max Spawn Stats", field_names)

    def test_embed_withdrawing_uses_actual_timestamp(self) -> None:
        withdrawing_time = datetime(2026, 3, 25, 0, 0, 0)
        incursion = Mock(withdrawing_timestamp=withdrawing_time, security_string="High Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_withdrawing(incursion)

        fields = {field.name: str(field.value) for field in embed.fields}
        expected_despawn = int((withdrawing_time + timedelta(days=1)).timestamp())
        self.assertEqual(fields["Despawn Before"], f"<t:{expected_despawn}:R>")
        self.assertEqual(embed.title, "Withdrawing High Sec: Region/Constellation")

    def test_embed_withdrawing_falls_back_to_now_when_missing_timestamp(self) -> None:
        incursion = Mock(withdrawing_timestamp=None, security_string="Low Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_withdrawing(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertIn("Despawn Before", field_names)

    def test_embed_ended_includes_withdrawing_stats_when_present(self) -> None:
        withdrawing_time = datetime(2026, 3, 20, 0, 0, 0)
        ended_time = datetime(2026, 3, 21, 0, 0, 0)
        incursion = Mock(ended_timestamp=ended_time, withdrawing_timestamp=withdrawing_time, security_string="High Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_ended(incursion)

        fields = {field.name: str(field.value) for field in embed.fields}
        self.assertIn("Max Spawn Stats", fields)
        self.assertIn("Withdrawing", fields["Max Spawn Stats"])

    def test_embed_ended_skips_stats_without_withdrawing_timestamp(self) -> None:
        ended_time = datetime(2026, 3, 21, 0, 0, 0)
        incursion = Mock(ended_timestamp=ended_time, withdrawing_timestamp=None, security_string="Low Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_ended(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertNotIn("Max Spawn Stats", field_names)

    def test_embed_ended_uses_now_when_ended_timestamp_missing(self) -> None:
        incursion = Mock(ended_timestamp=None, withdrawing_timestamp=None, security_string="Null Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_ended(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertIn("Spawn Window Opens", field_names)

    def test_embed_ended_swallows_withdrawing_stats_exception(self) -> None:
        ended_time = datetime(2026, 3, 21, 0, 0, 0)
        incursion = Mock(ended_timestamp=ended_time, withdrawing_timestamp="not-a-datetime", security_string="Low Sec")
        constellation = SimpleNamespace(name="Constellation")
        region = SimpleNamespace(name="Region")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, region, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
            patch("incursions.helpers.logger.debug") as mock_debug,
        ):
            embed = helpers.embed_ended(incursion)

        field_names = [field.name for field in embed.fields]
        self.assertNotIn("Max Spawn Stats", field_names)
        mock_debug.assert_any_call("Failed withdrawing stats", exc_info=True)

    def test_embed_boss_spawned_builds_expected_title(self) -> None:
        constellation = SimpleNamespace(name="Constellation")
        incursion = Mock(security_string="High Sec")

        with (
            patch("incursions.helpers._resolve_incursion_location", return_value=(constellation, None, None)),
            patch("incursions.helpers.embed_base", return_value=Embed(title="base")),
        ):
            embed = helpers.embed_boss_spawned(incursion)

        self.assertEqual(embed.title, "Boss has Spawned for High Sec: Constellation")
        self.assertEqual(embed.description, "Boss Spawned")
