"""Tests for the GUI module (PySide6 mocked since it's an optional dependency)."""

from __future__ import annotations

from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from redhat_oracle.core.config import OracleConfig
from redhat_oracle.core.session import ChatSession, save_session


class TestEscapeHtml:
    def test_escapes_angle_brackets(self) -> None:
        from redhat_oracle.gui.app import _escape_html

        assert _escape_html("<script>") == "&lt;script&gt;"

    def test_escapes_ampersand(self) -> None:
        from redhat_oracle.gui.app import _escape_html

        assert _escape_html("a & b") == "a &amp; b"

    def test_converts_newlines(self) -> None:
        from redhat_oracle.gui.app import _escape_html

        assert _escape_html("line1\nline2") == "line1<br>line2"

    def test_plain_text_unchanged(self) -> None:
        from redhat_oracle.gui.app import _escape_html

        assert _escape_html("hello world") == "hello world"

    def test_combined_escaping(self) -> None:
        from redhat_oracle.gui.app import _escape_html

        result = _escape_html("a < b & c > d\ne")
        assert result == "a &lt; b &amp; c &gt; d<br>e"


class TestImportQt:
    def test_raises_without_pyside6(self) -> None:
        from redhat_oracle.gui.app import _import_qt

        with (
            patch.dict("sys.modules", {"PySide6": None}),
            pytest.raises(RuntimeError, match="PySide6 is not installed"),
        ):
            _import_qt()


class TestAsyncWorker:
    def test_run_coroutine(self) -> None:
        from redhat_oracle.gui.app import AsyncWorker

        worker = AsyncWorker()

        mock_qt_core = MagicMock()

        class FakeQThread:
            def start(self) -> None:
                import asyncio
                import threading

                def run_loop() -> None:
                    loop = asyncio.new_event_loop()
                    asyncio.set_event_loop(loop)
                    worker._loop = loop
                    loop.run_forever()

                t = threading.Thread(target=run_loop, daemon=True)
                t.start()

            def wait(self, timeout: int) -> None:
                pass

        mock_qt_core.QThread = FakeQThread
        worker.start(mock_qt_core)

        async def add(a: int, b: int) -> int:
            return a + b

        result = worker.run(add(3, 4))
        assert result == 7

        worker.stop()

    def test_run_without_start_raises(self) -> None:
        from redhat_oracle.gui.app import AsyncWorker

        worker = AsyncWorker()

        async def coro() -> int:
            return 1

        with pytest.raises(RuntimeError, match="not started"):
            worker.run(coro())


class TestSessionManager:
    def test_new_creates_session(self) -> None:
        from redhat_oracle.gui.app import SessionManager

        config = OracleConfig()
        mgr = SessionManager(config, MagicMock())
        combo = MagicMock()
        combo.findText.return_value = -1
        mgr.init_widgets(combo)

        session = mgr.new()
        assert session is not None
        assert session.name
        assert mgr.session is session

    def test_load_by_name(self, tmp_path: Path) -> None:
        from redhat_oracle.gui.app import SessionManager

        config = OracleConfig(data_dir=str(tmp_path))
        session = ChatSession(name="test_load")
        session.add_message("user", "Hello")
        save_session(session, config)

        mgr = SessionManager(config, MagicMock())
        loaded = mgr.load("test_load")
        assert loaded is not None
        assert loaded.name == "test_load"
        assert len(loaded.messages) == 1

    def test_save_persists(self, tmp_path: Path) -> None:
        from redhat_oracle.gui.app import SessionManager

        config = OracleConfig(data_dir=str(tmp_path))
        mgr = SessionManager(config, MagicMock())
        combo = MagicMock()
        combo.findText.return_value = 0
        mgr.init_widgets(combo)

        mgr.session = ChatSession(name="save_test")
        mgr.session.add_message("user", "Hi")
        mgr.save()

        assert (tmp_path / "sessions" / "save_test.json").exists()

    def test_refresh_list_populates_combo(self, tmp_path: Path) -> None:
        from redhat_oracle.gui.app import SessionManager

        config = OracleConfig(data_dir=str(tmp_path))
        save_session(ChatSession(name="s1"), config)
        save_session(ChatSession(name="s2"), config)

        mgr = SessionManager(config, MagicMock())
        combo = MagicMock()
        combo.findText.return_value = -1
        mgr.init_widgets(combo)

        mgr.refresh_list()
        assert combo.addItem.call_count == 2


class TestChatPanel:
    def test_render_session(self) -> None:
        from redhat_oracle.gui.app import ChatPanel, SessionManager

        config = OracleConfig()
        mgr = SessionManager(config, MagicMock())
        mgr.session = ChatSession(name="test")
        mgr.session.add_message("user", "Hello")
        mgr.session.add_message("assistant", "Hi!")

        panel = ChatPanel(config, mgr, MagicMock())
        panel.display = MagicMock()

        panel.render_session()

        panel.display.clear.assert_called_once()
        assert panel.display.append.call_count == 4  # 2 messages + 2 blanks

    def test_send_creates_session_if_none(self) -> None:
        from redhat_oracle.gui.app import ChatPanel, SessionManager

        config = OracleConfig()
        mgr = SessionManager(config, MagicMock())
        combo = MagicMock()
        combo.findText.return_value = -1
        mgr.init_widgets(combo)
        mgr.session = None

        worker = MagicMock()

        # submit() calls on_result callback synchronously for testing
        def fake_submit(coro: Any, on_result: Any, on_error: Any) -> None:
            on_result(("Mocked response", []))
            coro.close()  # clean up unawaited coroutine

        worker.submit.side_effect = fake_submit

        panel = ChatPanel(config, mgr, worker)
        panel.display = MagicMock()
        panel.input_field = MagicMock()
        panel.input_field.text.return_value = "Hello"
        panel.send_button = MagicMock()
        panel.status_bar = MagicMock()

        with patch("redhat_oracle.gui.app.save_session"):
            panel.send_message()

        assert mgr.session is not None
        assert len(mgr.session.messages) == 2

    def test_empty_input_ignored(self) -> None:
        from redhat_oracle.gui.app import ChatPanel, SessionManager

        config = OracleConfig()
        mgr = SessionManager(config, MagicMock())

        panel = ChatPanel(config, mgr, MagicMock())
        panel.display = MagicMock()
        panel.input_field = MagicMock()
        panel.input_field.text.return_value = "   "

        panel.send_message()
        panel.display.append.assert_not_called()


class TestSourcePanel:
    def test_show_settings_callable(self) -> None:
        from redhat_oracle.gui.app import SourcePanel

        config = OracleConfig()
        panel = SourcePanel(config, MagicMock())
        assert callable(panel.show_settings)


class TestOracleGUI:
    def test_build_ui(self) -> None:
        mock_core = MagicMock()
        mock_gui = MagicMock()
        mock_widgets = MagicMock()

        with (
            patch("redhat_oracle.gui.app._import_qt") as mock_import,
            patch("redhat_oracle.gui.app._import_webengine", return_value=None),
            patch.dict("sys.modules", {"PySide6.QtSvg": None}),
        ):
            mock_import.return_value = (mock_core, mock_gui, mock_widgets)
            from redhat_oracle.gui.app import OracleGUI

            config = OracleConfig()
            gui = OracleGUI(config)
            gui._build_ui()

        assert gui._window is not None
        assert gui._chat_panel.display is not None
        assert gui._chat_panel.input_field is not None
        assert gui._chat_panel.send_button is not None

    def test_on_new_session(self) -> None:
        mock_core = MagicMock()
        mock_gui = MagicMock()
        mock_widgets = MagicMock()

        with patch("redhat_oracle.gui.app._import_qt") as mock_import:
            mock_import.return_value = (mock_core, mock_gui, mock_widgets)
            from redhat_oracle.gui.app import OracleGUI

            config = OracleConfig()
            gui = OracleGUI(config)
            gui._chat_panel.display = MagicMock()
            gui._chat_panel.status_bar = MagicMock()
            gui._session_mgr.combo = MagicMock()
            gui._session_mgr.combo.findText.return_value = -1

            gui._on_new_session()

        assert gui._session_mgr.session is not None
        gui._chat_panel.display.clear.assert_called_once()


class TestGraphExpander:
    def test_reset_clears_state(self) -> None:
        from redhat_oracle.gui.app import GraphExpander

        config = OracleConfig()
        expander = GraphExpander(config)
        expander._all_node_ids.add("test:node")
        expander._expanded_ids.add("test:node")

        expander.reset()
        assert len(expander._all_node_ids) == 0
        assert len(expander._expanded_ids) == 0

    def test_register_initial_nodes(self) -> None:
        from redhat_oracle.gui.app import GraphExpander

        config = OracleConfig()
        expander = GraphExpander(config)

        sources = [
            {"metadata": {"source": "github", "org": "o", "repo": "r", "path": "a.py"}},
            {"metadata": {"jira_issue_id": "AAP-123", "source": "jira"}},
        ]
        expander.register_initial_nodes(sources)
        assert "github:o/r/a.py" in expander._all_node_ids
        assert "jira:AAP-123" in expander._all_node_ids

    def test_make_node_id_jira(self) -> None:
        from redhat_oracle.gui.app import GraphExpander

        result = {"metadata": {"jira_issue_id": "TEST-42", "source": "jira"}}
        assert GraphExpander.make_node_id(result) == "jira:TEST-42"

    def test_make_node_id_code(self) -> None:
        from redhat_oracle.gui.app import GraphExpander

        result = {"metadata": {"source": "github", "org": "o", "repo": "r", "path": "f.py"}}
        assert GraphExpander.make_node_id(result) == "github:o/r/f.py"

    def test_expand_level_deduplication(self) -> None:
        from redhat_oracle.gui.app import GraphExpander

        config = OracleConfig()
        expander = GraphExpander(config)

        # Mock store that returns the same result for every query
        mock_store = MagicMock()
        duplicate_result = {
            "id": "dup-1",
            "text": "duplicate",
            "metadata": {"source": "github", "org": "o", "repo": "r", "path": "dup.py",
                         "author": "alice", "labels": "", "jira_issue_id": ""},
            "score": 1.0,
        }
        mock_store.search_by_metadata.return_value = [duplicate_result]
        mock_store.search.return_value = [duplicate_result]
        expander.set_store(mock_store)

        sources = [
            {"id": "src-1", "metadata": {"source": "github", "org": "o", "repo": "r",
                                          "path": "src.py", "author": "alice",
                                          "labels": "", "jira_issue_id": ""},
             "score": 0.9},
        ]
        expander.register_initial_nodes(sources)

        delta = expander.expand_level(sources, 2)

        # The duplicate should appear at most once as a new node
        node_ids = [n["id"] for n in delta["nodes"]]
        assert len(node_ids) == len(set(node_ids))

    def test_max_nodes_cap(self) -> None:
        from redhat_oracle.gui.app import GraphExpander

        config = OracleConfig()
        expander = GraphExpander(config, max_nodes=5)

        mock_store = MagicMock()

        def fake_search_by_metadata(field: str, value: str, top_k: int = 5) -> list:
            return [
                {"id": f"meta-{field}-{i}", "text": f"r{i}",
                 "metadata": {"source": "github", "org": "o", "repo": "r",
                              "path": f"meta_{field}_{i}.py",
                              "author": "", "labels": "", "jira_issue_id": ""},
                 "score": 1.0}
                for i in range(top_k)
            ]

        mock_store.search_by_metadata.side_effect = fake_search_by_metadata
        mock_store.search.return_value = []
        expander.set_store(mock_store)

        sources = [
            {"id": f"src-{i}", "metadata": {"source": "github", "org": "o", "repo": "r",
                                              "path": f"s{i}.py", "author": f"author{i}",
                                              "labels": "", "jira_issue_id": ""},
             "score": 0.9}
            for i in range(10)
        ]
        expander.register_initial_nodes(sources)

        delta = expander.expand_level(sources, 2)
        assert delta["total_nodes"] <= 5 + len(sources)  # max_nodes caps growth

    def test_opacity_by_level(self) -> None:
        from redhat_oracle.gui.app import GraphExpander

        config = OracleConfig()
        expander = GraphExpander(config)

        result = {"id": "test", "metadata": {"source": "github", "org": "o",
                                              "repo": "r", "path": "f.py"},
                  "score": 0.8}

        node_l1 = expander._make_graph_node(result, level=1)
        node_l2 = expander._make_graph_node(result, level=2)
        node_l3 = expander._make_graph_node(result, level=3)

        assert node_l1["opacity"] == 1.0
        assert node_l2["opacity"] == 0.7
        assert node_l3["opacity"] == 0.4

    def test_expand_without_store_returns_empty(self) -> None:
        from redhat_oracle.gui.app import GraphExpander

        config = OracleConfig()
        expander = GraphExpander(config)

        delta = expander.expand_level([], 2)
        assert delta["nodes"] == []
        assert delta["links"] == []


class TestLaunchGUI:
    def test_launch_gui_function_exists(self) -> None:
        from redhat_oracle.gui.app import launch_gui

        assert callable(launch_gui)
