# Standard Library
import bz2
import json
import lzma
import os
import tempfile
from datetime import datetime, timedelta, timezone
from unittest.mock import patch

# Alliance Auth (Core)
from allianceauth.eveonline.models import EveFactionInfo
from allianceauth.utils.testing import NoSocketsTestCase

# Alliance Auth (External Libs)
from eve_sde.models import Constellation, SolarSystem

from incursions.everef import import_staging_backfill, import_staging_history
from incursions.models.incursion import Incursion, IncursionInfluence
from incursions.tests.test_tasks_update_incursions import NoSocketsTransactionTestCase


def _write_bz2_json(dir_path: str, filename: str, payload: object) -> None:
    with bz2.open(os.path.join(dir_path, filename), "wb") as f:
        f.write(json.dumps(payload).encode())


class ImportStagingHistoryTests(NoSocketsTestCase):
    def setUp(self) -> None:
        self.faction = EveFactionInfo.objects.create(faction_id=500019, faction_name="Sansha's Nation")
        self.constellation = Constellation.objects.create(id=20000001, name="Test Constellation")
        self.staging_system = SolarSystem.objects.create(
            id=30000142, name="Jita", constellation=self.constellation, security_status=0.9,
        )

    def test_ignores_non_matching_files_and_recurses_subdirectories(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            os.makedirs(os.path.join(tmp, "subdir"))
            with open(os.path.join(tmp, "readme.txt"), "w") as f:
                f.write("ignore me")
            ts = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
            _write_bz2_json(
                os.path.join(tmp, "subdir"), f"incursions-{ts:%Y-%m-%d_%H-%M-%S}.json.bz2",
                [{
                    "state": "established", "constellation_id": 20000001, "faction_id": 500019,
                    "has_boss": "false", "staging_solar_system_id": 30000142, "type": "Incursion",
                }],
            )
            import_staging_history(tmp)

        # Note: state ends up "ended" here regardless, because the per-file
        # "mark ended" sweep in import_staging_history always matches an
        # established_timestamp <= this file's own ts (see conversation notes).
        self.assertEqual(Incursion.objects.count(), 1)
        self.assertEqual(Incursion.objects.get().established_timestamp, ts)

    def test_skips_file_with_unparsable_timestamp(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(tmp, "incursions-not-a-date.json.bz2", [])
            with patch("incursions.everef.logger.warning") as mock_warning:
                import_staging_history(tmp)

        mock_warning.assert_called_once()
        self.assertFalse(Incursion.objects.exists())

    def test_skips_file_with_invalid_json(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            path = os.path.join(tmp, "incursions-2026-03-01_12-00-00.json.bz2")
            with bz2.open(path, "wb") as f:
                f.write(b"not json")
            with patch("incursions.everef.logger.exception") as mock_exception:
                import_staging_history(tmp)

        mock_exception.assert_called_once()
        self.assertFalse(Incursion.objects.exists())

    def test_skips_entry_with_missing_constellation(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(
                tmp, f"incursions-{ts:%Y-%m-%d_%H-%M-%S}.json.bz2",
                [{"state": "established", "constellation_id": 999999}],
            )
            import_staging_history(tmp)

        self.assertFalse(Incursion.objects.exists())

    def test_creates_new_established_incursion(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(
                tmp, f"incursions-{ts:%Y-%m-%d_%H-%M-%S}.json.bz2",
                [{
                    "state": "established", "constellation_id": 20000001, "faction_id": 500019,
                    "has_boss": "true", "staging_solar_system_id": 30000142, "type": "Incursion",
                }],
            )
            import_staging_history(tmp)

        # Note: state ends up "ended" here regardless, because the per-file
        # "mark ended" sweep in import_staging_history always matches an
        # established_timestamp <= this file's own ts.
        incursion = Incursion.objects.get()
        self.assertTrue(incursion.has_boss)
        self.assertEqual(incursion.established_timestamp, ts)

    def test_creation_failure_is_swallowed(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(
                tmp, f"incursions-{ts:%Y-%m-%d_%H-%M-%S}.json.bz2",
                [{
                    "state": "established", "constellation_id": 20000001, "faction_id": 500019,
                    "has_boss": "false", "staging_solar_system_id": 999999, "type": "Incursion",
                }],
            )
            import_staging_history(tmp)

        self.assertFalse(Incursion.objects.exists())

    def test_multiple_established_matches_are_skipped(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
        Incursion.objects.create(
            constellation=self.constellation, faction=self.faction, staging_solar_system=self.staging_system,
            state="established", established_timestamp=ts,
        )
        Incursion.objects.create(
            constellation=self.constellation, faction=self.faction, staging_solar_system=self.staging_system,
            state="established", established_timestamp=ts + timedelta(hours=1),
        )
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(
                tmp, f"incursions-{ts:%Y-%m-%d_%H-%M-%S}.json.bz2",
                [{
                    "state": "established", "constellation_id": 20000001, "faction_id": 500019,
                    "has_boss": "false", "staging_solar_system_id": 30000142, "type": "Incursion",
                }],
            )
            with patch("incursions.everef.logger.exception") as mock_exception:
                import_staging_history(tmp)

        self.assertEqual(Incursion.objects.count(), 2)
        mock_exception.assert_called_once()

    def test_upgrades_existing_established_to_mobilizing(self) -> None:
        established_ts = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
        mobilizing_ts = established_ts + timedelta(days=3)
        incursion = Incursion.objects.create(
            constellation=self.constellation, faction=self.faction, staging_solar_system=self.staging_system,
            state="established", established_timestamp=established_ts,
        )
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(
                tmp, f"incursions-{mobilizing_ts:%Y-%m-%d_%H-%M-%S}.json.bz2",
                [{"state": "mobilizing", "constellation_id": 20000001}],
            )
            import_staging_history(tmp)

        # Note: state ends up "ended" via the per-file "mark ended" sweep
        # (established_timestamp is always <= this file's own ts).
        incursion.refresh_from_db()
        self.assertEqual(incursion.mobilizing_timestamp, mobilizing_ts)

    def test_withdrawing_falls_back_to_mobilizing_window(self) -> None:
        established_ts = datetime(2026, 2, 1, 12, 0, tzinfo=timezone.utc)
        mobilizing_ts = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
        withdrawing_ts = mobilizing_ts + timedelta(days=1)
        incursion = Incursion.objects.create(
            constellation=self.constellation, faction=self.faction, staging_solar_system=self.staging_system,
            state="mobilizing", established_timestamp=established_ts, mobilizing_timestamp=mobilizing_ts,
        )
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(
                tmp, f"incursions-{withdrawing_ts:%Y-%m-%d_%H-%M-%S}.json.bz2",
                [{"state": "withdrawing", "constellation_id": 20000001}],
            )
            import_staging_history(tmp)

        # Note: state ends up "ended" via the per-file "mark ended" sweep
        # (established_timestamp is always <= this file's own ts).
        incursion.refresh_from_db()
        self.assertEqual(incursion.withdrawing_timestamp, withdrawing_ts)

    def test_no_candidate_incursion_is_skipped(self) -> None:
        ts = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(
                tmp, f"incursions-{ts:%Y-%m-%d_%H-%M-%S}.json.bz2",
                [{"state": "mobilizing", "constellation_id": 20000001}],
            )
            import_staging_history(tmp)

        self.assertFalse(Incursion.objects.exists())

    def test_marks_existing_incursions_as_ended(self) -> None:
        established_ts = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
        incursion = Incursion.objects.create(
            constellation=self.constellation, faction=self.faction, staging_solar_system=self.staging_system,
            state="withdrawing", established_timestamp=established_ts,
        )
        later_ts = established_ts + timedelta(days=20)
        with tempfile.TemporaryDirectory() as tmp:
            _write_bz2_json(tmp, f"incursions-{later_ts:%Y-%m-%d_%H-%M-%S}.json.bz2", [])
            import_staging_history(tmp)

        incursion.refresh_from_db()
        self.assertEqual(incursion.state, Incursion.States.ENDED)
        self.assertEqual(incursion.ended_timestamp, later_ts)


def _write_backfill(tmp: str, payload: object) -> None:
    os.makedirs(os.path.join(tmp, "backfills"), exist_ok=True)
    path = os.path.join(tmp, "backfills", "eve-incursions-de-2023-10-12.json.xz")
    with lzma.open(path, "wb") as f:
        f.write(json.dumps(payload).encode())


class ImportStagingBackfillTests(NoSocketsTestCase):
    def setUp(self) -> None:
        self.faction = EveFactionInfo.objects.create(faction_id=500019, faction_name="Sansha's Nation")
        self.constellation = Constellation.objects.create(id=20000011, name="Test Constellation")
        self.unknown_system = SolarSystem.objects.create(
            id=30000001, name="UNKNOWN", constellation=self.constellation, security_status=0.9,
        )

    def test_missing_file_logs_warning_and_returns(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp), \
                    patch("incursions.everef.logger.warning") as mock_warning:
                import_staging_backfill()

        mock_warning.assert_called_once()

    def test_invalid_json_logs_exception_and_returns(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            os.makedirs(os.path.join(tmp, "backfills"))
            path = os.path.join(tmp, "backfills", "eve-incursions-de-2023-10-12.json.xz")
            with lzma.open(path, "wb") as f:
                f.write(b"not json")
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp), \
                    patch("incursions.everef.logger.exception") as mock_exception:
                import_staging_backfill()

        mock_exception.assert_called_once()
        self.assertFalse(Incursion.objects.exists())

    def test_skips_entry_missing_constellation(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            _write_backfill(tmp, [{"spawn": {"constellation": {"id": 999999}}}])
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp):
                import_staging_backfill()

        self.assertFalse(Incursion.objects.exists())

    def test_skips_entry_without_established_or_ended_dates(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            _write_backfill(tmp, [{
                "spawn": {"constellation": {"id": 20000011}, "establishedAt": None, "endedAt": None},
            }])
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp):
                import_staging_backfill()

        self.assertFalse(Incursion.objects.exists())

    def test_creates_incursion_with_unknown_staging_system(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            _write_backfill(tmp, [{
                "spawn": {
                    "constellation": {"id": 20000011},
                    "establishedAt": "2015-02-06T18:13:45.000Z",
                    "endedAt": "2015-02-09T16:00:01.000Z",
                    "influenceLogs": [],
                },
            }])
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp):
                import_staging_backfill()

        incursion = Incursion.objects.get()
        self.assertEqual(incursion.state, Incursion.States.ENDED)
        self.assertEqual(incursion.staging_solar_system, self.unknown_system)
        self.assertEqual(incursion.established_timestamp, datetime(2015, 2, 6, 18, 13, 45, tzinfo=timezone.utc))
        self.assertEqual(incursion.ended_timestamp, datetime(2015, 2, 9, 16, 0, 1, tzinfo=timezone.utc))

    def test_reuses_existing_matching_incursion(self) -> None:
        existing = Incursion.objects.create(
            constellation=self.constellation, faction=self.faction, staging_solar_system=self.unknown_system,
            state=Incursion.States.ENDED,
            established_timestamp=datetime(2015, 2, 6, 18, 13, 45, tzinfo=timezone.utc),
            ended_timestamp=datetime(2015, 2, 9, 16, 0, 1, tzinfo=timezone.utc),
        )
        with tempfile.TemporaryDirectory() as tmp:
            _write_backfill(tmp, [{
                "spawn": {
                    "constellation": {"id": 20000011},
                    "establishedAt": "2015-02-06T18:13:45.000Z",
                    "endedAt": "2015-02-09T16:00:01.000Z",
                    "influenceLogs": [],
                },
            }])
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp):
                import_staging_backfill()

        self.assertEqual(Incursion.objects.count(), 1)
        self.assertEqual(Incursion.objects.get().pk, existing.pk)

    def test_creation_failure_without_unknown_system_is_swallowed(self) -> None:
        self.unknown_system.delete()
        with tempfile.TemporaryDirectory() as tmp:
            _write_backfill(tmp, [{
                "spawn": {
                    "constellation": {"id": 20000011},
                    "establishedAt": "2015-02-06T18:13:45.000Z",
                    "endedAt": "2015-02-09T16:00:01.000Z",
                    "influenceLogs": [],
                },
            }])
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp):
                import_staging_backfill()

        self.assertFalse(Incursion.objects.exists())

    def test_sets_supplemental_mobilizing_and_withdrawing_timestamps(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            _write_backfill(tmp, [
                {
                    "state": "Mobilizing",
                    "date": "2015-02-07T00:00:00.000Z",
                    "spawn": {
                        "constellation": {"id": 20000011},
                        "establishedAt": "2015-02-06T18:13:45.000Z",
                        "endedAt": "2015-02-09T16:00:01.000Z",
                        "influenceLogs": [],
                    },
                },
                {
                    "state": "Withdrawing",
                    "date": "2015-02-08T00:00:00.000Z",
                    "spawn": {
                        "constellation": {"id": 20000011},
                        "establishedAt": "2015-02-06T18:13:45.000Z",
                        "endedAt": "2015-02-09T16:00:01.000Z",
                        "influenceLogs": [],
                    },
                },
            ])
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp):
                import_staging_backfill()

        incursion = Incursion.objects.get()
        self.assertEqual(incursion.mobilizing_timestamp, datetime(2015, 2, 7, tzinfo=timezone.utc))
        self.assertEqual(incursion.withdrawing_timestamp, datetime(2015, 2, 8, tzinfo=timezone.utc))

    def test_creates_influence_logs(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            _write_backfill(tmp, [{
                "spawn": {
                    "constellation": {"id": 20000011},
                    "establishedAt": "2015-02-06T18:13:45.000Z",
                    "endedAt": "2015-02-09T16:00:01.000Z",
                    "influenceLogs": [{"date": "2015-02-07T09:00:00.000Z", "influence": 0.5}],
                },
            }])
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp):
                import_staging_backfill()

        log = IncursionInfluence.objects.get()
        self.assertEqual(log.influence, 0.5)


class ImportStagingBackfillDuplicateInfluenceTests(NoSocketsTransactionTestCase):
    """Uses TransactionTestCase: a real save() raising IntegrityError needs
    autocommit semantics to keep the connection usable afterwards, matching
    the precedent in test_tasks_update_incursions.py.
    """

    def setUp(self) -> None:
        self.faction = EveFactionInfo.objects.create(faction_id=500019, faction_name="Sansha's Nation")
        self.constellation = Constellation.objects.create(id=20000011, name="Test Constellation")
        self.unknown_system = SolarSystem.objects.create(
            id=30000001, name="UNKNOWN", constellation=self.constellation, security_status=0.9,
        )

    def test_duplicate_influence_log_is_swallowed(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            _write_backfill(tmp, [{
                "spawn": {
                    "constellation": {"id": 20000011},
                    "establishedAt": "2015-02-06T18:13:45.000Z",
                    "endedAt": "2015-02-09T16:00:01.000Z",
                    "influenceLogs": [
                        {"date": "2015-02-07T09:00:00.000Z", "influence": 0.1},
                        {"date": "2015-02-07T09:00:00.000Z", "influence": 0.2},
                    ],
                },
            }])
            with patch("incursions.everef.EVEREF_STAGING_FOLDER", tmp):
                import_staging_backfill()

        self.assertEqual(IncursionInfluence.objects.count(), 1)
