"""Regressions for the v2.1 location-selection bug.

The UI event handlers are tested without requiring a graphic session or Textual:
lightweight fake classes satisfy imports; business logic/events remain real.
"""
import asyncio
import importlib
import sys
import types
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

from archweather import core


def load_ui_without_textual():
    fake = {}
    for name in ("textual", "textual.app", "textual.binding", "textual.containers", "textual.widgets"):
        fake[name] = types.ModuleType(name)
    fake["textual.app"].App = type("App", (), {})
    fake["textual.app"].ComposeResult = object
    fake["textual.binding"].Binding = lambda *a, **kw: object()
    for name in ("Vertical", "Horizontal"):
        setattr(fake["textual.containers"], name, type(name, (), {}))
    for name in ("Header", "Footer", "Input", "Static", "DataTable", "Sparkline", "TabbedContent", "TabPane", "Button"):
        setattr(fake["textual.widgets"], name, type(name, (), {}))
    for name, event in (("Input", "Submitted"), ("Button", "Pressed"), ("DataTable", "RowSelected"), ("DataTable", "CellSelected")):
        setattr(getattr(fake["textual.widgets"], name), event, object)
    sys.modules.pop("archweather.ui", None)
    with patch.dict(sys.modules, fake):
        ui = importlib.import_module("archweather.ui")
    sys.modules.pop("archweather.ui", None)
    return ui


UI = load_ui_without_textual()


class TableRegressionTests(unittest.TestCase):
    def setUp(self):
        self.app = UI.WeatherApp.__new__(UI.WeatherApp)
        self.app.search_results = [
            {"name": "De Steeg", "admin1": "Gelderland", "latitude": 52.02, "longitude": 6.061},
            {"name": "De Steegh", "admin1": "Limburg", "latitude": 51.455, "longitude": 6.152},
        ]
        self.app.pending_query = "De Steeg"
        self.app.cfg = {"favorites": ["Delft", "Utrecht"]}
        self.app.choose_place = MagicMock()
        self.app.run_search = MagicMock()

    def test_row_enter_chooses_correct_result(self):
        table = SimpleNamespace(id="resulttable", get_row_index=lambda _: 1)
        self.app.on_data_table_row_selected(SimpleNamespace(data_table=table, row_key="r2"))
        self.app.choose_place.assert_called_once_with(self.app.search_results[1], "De Steeg")

    def test_cell_click_chooses_correct_result(self):
        table = SimpleNamespace(id="resulttable")
        self.app.on_data_table_cell_selected(SimpleNamespace(data_table=table, coordinate=SimpleNamespace(row=0)))
        self.app.choose_place.assert_called_once_with(self.app.search_results[0], "De Steeg")

    def test_location_button_selects_highlighted_result(self):
        table = SimpleNamespace(cursor_coordinate=SimpleNamespace(row=1))
        self.app.query_one = lambda *_: table
        self.app.on_button_pressed(SimpleNamespace(button=SimpleNamespace(id="selectlocationbutton")))
        self.app.choose_place.assert_called_once_with(self.app.search_results[1], "De Steeg")

    def test_favorite_selection(self):
        table = SimpleNamespace(id="favtable", get_row_index=lambda _: 1)
        self.app.on_data_table_row_selected(SimpleNamespace(data_table=table, row_key="r2"))
        self.app.run_search.assert_called_once_with("Utrecht")

    def test_invalid_row_does_not_select(self):
        self.app.select_table_index("resulttable", 5)
        self.app.choose_place.assert_not_called()

    def test_tables_are_in_row_mode(self):
        ui_source = (Path(__file__).resolve().parents[1] / "archweather" / "ui.py").read_text()
        self.assertIn('DataTable(id="resulttable", cursor_type="row"', ui_source)
        self.assertIn('DataTable(id="favtable", cursor_type="row"', ui_source)


class AsyncRegressionTests(unittest.IsolatedAsyncioTestCase):
    def setUp(self):
        self.app = UI.WeatherApp.__new__(UI.WeatherApp)
        self.app.cfg = {"api_seconds": 300}
        self.app.place = {"name": "De Steeg", "latitude": 52.02, "longitude": 6.061}
        self.app.loading = False
        self.app.last_request = 0.0
        self.app.weather = None
        self.app.fetched_at = None
        self.app.offline = False
        self.app.notice = ""
        self.app.search_seq = 5
        self.app.render_status = MagicMock()
        self.app.render_weather = MagicMock()
        self.app.show_error = MagicMock()
        self.app.populate_local_tables = MagicMock()

    async def test_startup_auto_picks_first_location(self):
        self.app.choose_place = MagicMock()
        found = [self.app.place, {"name": "De Steegh", "latitude": 51.45, "longitude": 6.15}]
        with patch.object(core, "find_places", return_value=found):
            await self.app.search_task("De Steeg", 5, auto_choose=True)
        self.app.choose_place.assert_called_once_with(found[0], "De Steeg", auto_selected=True)

    async def test_manual_search_shows_choices_without_autopick(self):
        self.app.choose_place = MagicMock()
        self.app.search_results = []
        table = MagicMock()
        tabs = SimpleNamespace(active="hours")
        self.app.query_one = lambda key, *_: table if key == "#resulttable" else tabs
        found = [self.app.place, {"name": "De Steegh", "latitude": 51.45, "longitude": 6.15}]
        with patch.object(core, "find_places", return_value=found):
            await self.app.search_task("De Steeg", 5)
        self.app.choose_place.assert_not_called()
        self.assertEqual(len(table.add_row.call_args_list), 2)
        self.assertEqual(tabs.active, "results")

    async def test_fetch_success_renders_even_if_cache_cannot_be_written(self):
        data = {"current": {"temperature_2m": 17}, "hourly": {}, "daily": {}}
        with patch.object(core, "forecast", return_value=data), \
             patch.object(core, "cache_save", side_effect=PermissionError("readonly")), \
             patch.object(core, "save_observation"):
            await self.app.fetch_task(force=True)
        self.assertEqual(self.app.weather, data)
        self.assertFalse(self.app.loading)
        self.app.render_weather.assert_called_once()
        self.app.show_error.assert_not_called()

    async def test_fetch_failure_shows_diagnostic_not_spinner(self):
        with patch.object(core, "forecast", side_effect=OSError("network disconnected")), \
             patch.object(core, "cache_load", return_value=None):
            await self.app.fetch_task(force=True)
        self.assertFalse(self.app.loading)
        self.app.render_weather.assert_not_called()
        self.app.show_error.assert_called_once_with("network disconnected")


if __name__ == "__main__":
    unittest.main()
