"""Tests for ``a360_mcp.services.execution``.

Two-layer mocking convention: the network layer is mocked at ``get_record`` /
``list_all`` (the data-access helpers), never at ``make_request`` — that keeps
the tests about execution *parsing*, not HTTP mechanics.
"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from a360_mcp.auth.auth_manager import A360AuthError
from a360_mcp.services import execution as ex
from a360_mcp.tools.a360_api import A360ApiError


# --------------------------------------------------------------------------- #
# is_failure_status
# --------------------------------------------------------------------------- #
class TestIsFailureStatus:
    def test_known_failed_statuses(self):
        for status in ("RUN_FAILED", "RUN_ABORTED", "RUN_TIMED_OUT", "DEPLOY_FAILED"):
            assert ex.is_failure_status(status) is True

    def test_success_statuses_are_not_failures(self):
        for status in ("RUN_SUCCESS", "RUN_COMPLETED", "COMPLETED", "RUNNING", "QUEUED"):
            assert ex.is_failure_status(status) is False

    def test_unknown_failure_variant_still_detected(self):
        # A new failure enum must not read as success.
        assert ex.is_failure_status("RUN_FAILED_NEW_THING") is True
        assert ex.is_failure_status("DEPLOY_FAILED_X") is True

    def test_empty_and_none(self):
        assert ex.is_failure_status(None) is False
        assert ex.is_failure_status("") is False
        assert ex.is_failure_status("   ") is False

    def test_case_insensitive_markers(self):
        # API enums are underscore tokens; lowercase variants still match.
        assert ex.is_failure_status("run_failed") is True
        assert ex.is_failure_status("run_timed_out") is True
        assert ex.is_failure_status("deploy_failed") is True


# --------------------------------------------------------------------------- #
# extract_error
# --------------------------------------------------------------------------- #
class TestExtractError:
    def test_structured_error_present(self):
        raw = {"status": "RUN_FAILED", "error": {"code": "X", "message": "boom"}, "message": "boom"}
        result = ex.extract_error(raw)
        assert result["code"] == "X"
        assert result["source"] == "error"
        assert result["message"] == "boom"

    def test_structured_error_without_message_inherits_top_message(self):
        raw = {"error": {"code": "X"}, "message": "fallback"}
        result = ex.extract_error(raw)
        assert result["source"] == "error"
        assert result["message"] == "fallback"

    def test_falls_back_to_message(self):
        raw = {"status": "RUN_FAILED", "message": "Line number: 37 file not found"}
        result = ex.extract_error(raw)
        assert result["source"] == "message"
        assert result["message"] == "Line number: 37 file not found"

    def test_no_error_no_message_returns_none(self):
        assert ex.extract_error({"status": "RUN_SUCCESS"}) is None

    def test_empty_error_dict_falls_back_to_message(self):
        raw = {"error": {}, "message": "use me"}
        result = ex.extract_error(raw)
        assert result["source"] == "message"
        assert result["message"] == "use me"

    def test_empty_message_returns_none(self):
        assert ex.extract_error({"message": "   "}) is None

    def test_non_dict_error_ignored(self):
        raw = {"error": "string error", "message": "real message"}
        result = ex.extract_error(raw)
        assert result["source"] == "message"


# --------------------------------------------------------------------------- #
# extract_line_number
# --------------------------------------------------------------------------- #
class TestExtractLineNumber:
    def test_from_error_dict_int(self):
        assert ex.extract_line_number({"lineNumber": 37}, None) == 37

    def test_from_error_dict_alternate_keys(self):
        assert ex.extract_line_number({"line": 42}, None) == 42
        assert ex.extract_line_number({"failedLineNumber": 9}, None) == 9

    def test_from_error_dict_string_digit(self):
        assert ex.extract_line_number({"lineNumber": "15"}, None) == 15

    def test_zero_and_negative_rejected(self):
        assert ex.extract_line_number({"lineNumber": 0}, None) is None
        assert ex.extract_line_number({"lineNumber": -1}, None) is None

    def test_from_message_line_number(self):
        assert ex.extract_line_number(None, "Line number: 37 file not found") == 37

    def test_from_message_at_line(self):
        assert ex.extract_line_number(None, "Exception at line 42") == 42

    def test_from_message_step_number(self):
        assert ex.extract_line_number(None, "failed at step number 12") == 12

    def test_no_line_number_anywhere(self):
        assert ex.extract_line_number({"code": "X"}, "something broke") is None
        assert ex.extract_line_number(None, None) is None

    def test_error_takes_precedence_over_message(self):
        # error has line, message has a different line — error wins.
        assert ex.extract_line_number({"lineNumber": 10}, "Line number: 20") == 10


# --------------------------------------------------------------------------- #
# parse_execution
# --------------------------------------------------------------------------- #
class TestParseExecution:
    def test_full_record(self):
        raw = {
            "id": "exec-1",
            "status": "RUN_FAILED",
            "message": "Line number: 37 boom",
            "fileId": "file-9",
            "deploymentId": "dep-1",
            "botName": "My Bot",
            "startedOn": "2024-01-01T00:00:00Z",
            "completedOn": "2024-01-01T00:01:00Z",
            "error": {"lineNumber": 37, "message": "boom"},
        }
        summary = ex.parse_execution(raw)
        assert summary.id == "exec-1"
        assert summary.status == "RUN_FAILED"
        assert summary.message == "Line number: 37 boom"
        assert summary.file_id == "file-9"
        assert summary.deployment_id == "dep-1"
        assert summary.bot_name == "My Bot"
        assert summary.started_on == "2024-01-01T00:00:00Z"
        assert summary.completed_on == "2024-01-01T00:01:00Z"
        assert summary.error["lineNumber"] == 37
        assert summary.raw is raw

    def test_file_id_alternate_keys(self):
        # First non-empty match wins across known keys.
        assert ex.parse_execution({"botFileId": "b1"}).file_id == "b1"
        assert ex.parse_execution({"botContentFileId": "b2"}).file_id == "b2"

    def test_file_id_zero_placeholder_ignored(self):
        # Numeric 0 is almost certainly an unset placeholder.
        assert ex.parse_execution({"fileId": 0}).file_id is None

    def test_missing_fields_become_none(self):
        summary = ex.parse_execution({"id": "x", "status": "RUN_SUCCESS"})
        assert summary.file_id is None
        assert summary.deployment_id is None
        assert summary.bot_name is None
        assert summary.error is None

    def test_id_falls_back_to_execution_id(self):
        assert ex.parse_execution({"executionId": "e9"}).id == "e9"

    def test_empty_record_safe(self):
        summary = ex.parse_execution({})
        assert summary.id == ""
        assert summary.status == ""

    def test_rejects_non_dict(self):
        with pytest.raises(TypeError):
            ex.parse_execution("not a dict")  # type: ignore[arg-type]


# --------------------------------------------------------------------------- #
# fetch_execution (network)
# --------------------------------------------------------------------------- #
class TestFetchExecution:
    def test_calls_get_record_with_correct_path(self, monkeypatch):
        captured = {}

        def fake_get_record(auth, path):
            captured["path"] = path
            return {"id": "exec-1"}

        monkeypatch.setattr(ex, "get_record", fake_get_record)
        auth = MagicMock()
        result = ex.fetch_execution(auth, "exec-1")
        assert result == {"id": "exec-1"}
        assert captured["path"] == "/v3/activity/execution/exec-1"

    def test_empty_execution_id_rejected(self):
        with pytest.raises(ValueError):
            ex.fetch_execution(MagicMock(), "")

    def test_api_error_propagates(self, monkeypatch):
        def raise_api(auth, path):
            raise A360ApiError("boom", status_code=404, path=path, body="")

        monkeypatch.setattr(ex, "get_record", raise_api)
        with pytest.raises(A360ApiError):
            ex.fetch_execution(MagicMock(), "exec-1")

    def test_auth_error_propagates_never_silenced(self, monkeypatch):
        def raise_auth(auth, path):
            raise A360AuthError("not logged in")

        monkeypatch.setattr(ex, "get_record", raise_auth)
        with pytest.raises(A360AuthError):
            ex.fetch_execution(MagicMock(), "exec-1")


# --------------------------------------------------------------------------- #
# find_executions (network)
# --------------------------------------------------------------------------- #
class TestFindExecutions:
    def test_no_filters(self, monkeypatch):
        captured = {}

        def fake_list_all(auth, path, **kwargs):
            captured["path"] = path
            captured["filter"] = kwargs.get("filter")
            return [{"id": "e1"}]

        monkeypatch.setattr(ex, "list_all", fake_list_all)
        result = ex.find_executions(MagicMock())
        assert result == [{"id": "e1"}]
        assert captured["path"] == "/v3/activity/list"
        assert captured["filter"] is None

    def test_status_filter(self, monkeypatch):
        captured = {}

        def fake_list_all(auth, path, **kwargs):
            captured["filter"] = kwargs.get("filter")
            return []

        monkeypatch.setattr(ex, "list_all", fake_list_all)
        ex.find_executions(MagicMock(), status="RUN_FAILED")
        flt = captured["filter"]
        assert flt is not None
        # single condition is bare
        assert flt["field"] == "status"
        assert flt["value"] == "RUN_FAILED"

    def test_multiple_filters_combined(self, monkeypatch):
        captured = {}

        def fake_list_all(auth, path, **kwargs):
            captured["filter"] = kwargs.get("filter")
            return []

        monkeypatch.setattr(ex, "list_all", fake_list_all)
        ex.find_executions(MagicMock(), deployment_id="d1", status="RUN_FAILED")
        flt = captured["filter"]
        # multiple conditions wrapped in and
        assert flt["operator"] == "and"
        assert len(flt["operands"]) == 2

    def test_auth_error_propagates(self, monkeypatch):
        def raise_auth(auth, path, **kwargs):
            raise A360AuthError("nope")

        monkeypatch.setattr(ex, "list_all", raise_auth)
        with pytest.raises(A360AuthError):
            ex.find_executions(MagicMock())
