"""MythicAdapter M4 tests — list_callback_tasks, mocked HTTP."""
from __future__ import annotations

import pytest
import requests
import requests_mock as rm_module

from backend.app.services.c2.adapter import C2Error, C2HistoricalTask
from backend.app.services.c2.mythic import MythicAdapter

_BASE_URL = "https://mythic.lab:7443"
_GQL_URL = _BASE_URL + "/graphql/"
_TOKEN = "fake-api-token"


@pytest.fixture()
def adapter():
    return MythicAdapter(url=_BASE_URL, api_token=_TOKEN, verify_tls=False)


def _task_list_payload(tasks: list[dict]) -> dict:
    return {"data": {"task": tasks}}


def _count_payload(count: int) -> dict:
    return {"data": {"task_aggregate": {"aggregate": {"count": count}}}}


class TestMythicAdapterListCallbackTasks:
    def test_returns_tasks_from_graphql(self, adapter):
        tasks_payload = _task_list_payload([
            {
                "display_id": 7,
                "command_name": "whoami",
                "params": "",
                "status": "completed",
                "completed": True,
                "timestamp": "2026-06-10T12:00:00Z",
            }
        ])
        count_payload = _count_payload(1)

        with rm_module.Mocker() as m:
            m.post(_GQL_URL, [{"json": tasks_payload}, {"json": count_payload}])
            page = adapter.list_callback_tasks(callback_display_id=1, page=1, page_size=25)

        assert page.total == 1
        assert len(page.items) == 1
        item = page.items[0]
        assert isinstance(item, C2HistoricalTask)
        assert item.display_id == 7
        assert item.command == "whoami"
        assert item.completed is True

    def test_pagination_offset_calculation(self, adapter):
        """page=2, page_size=10 → offset=10 must be sent to Mythic."""
        tasks_payload = _task_list_payload([])
        count_payload = _count_payload(0)

        with rm_module.Mocker() as m:
            m.post(_GQL_URL, [{"json": tasks_payload}, {"json": count_payload}])
            adapter.list_callback_tasks(callback_display_id=1, page=2, page_size=10)

            # First request is the task list; check variables.
            first_body = m.request_history[0].json()
            variables = first_body.get("variables", {})

        assert variables.get("offset") == 10
        assert variables.get("limit") == 10

    def test_sends_apitoken_header(self, adapter):
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, [
                {"json": _task_list_payload([])},
                {"json": _count_payload(0)},
            ])
            adapter.list_callback_tasks(callback_display_id=1)
            for req in m.request_history:
                assert req.headers.get("apitoken") == _TOKEN

    def test_empty_task_list(self, adapter):
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, [
                {"json": _task_list_payload([])},
                {"json": _count_payload(0)},
            ])
            page = adapter.list_callback_tasks(callback_display_id=1)

        assert page.total == 0
        assert page.items == []

    def test_network_error_raises_c2error(self, adapter):
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, exc=requests.exceptions.ConnectionError("refused"))
            with pytest.raises(C2Error):
                adapter.list_callback_tasks(callback_display_id=1)

    def test_http_error_raises_c2error(self, adapter):
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, status_code=500, text="error")
            with pytest.raises(C2Error):
                adapter.list_callback_tasks(callback_display_id=1)

    def test_no_redirect_followed(self, adapter):
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, status_code=301, headers={"Location": "https://evil.example/"})
            with pytest.raises(C2Error):
                adapter.list_callback_tasks(callback_display_id=1)
            # Both requests (tasks + count) should each only make one attempt.
            for req in m.request_history:
                assert req.method == "POST"

    def test_page_and_page_size_in_response(self, adapter):
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, [
                {"json": _task_list_payload([])},
                {"json": _count_payload(50)},
            ])
            page = adapter.list_callback_tasks(callback_display_id=1, page=3, page_size=10)

        assert page.page == 3
        assert page.page_size == 10
        assert page.total == 50


class TestMythicAdapterGetTaskCommandField:
    """Ensure command_name is surfaced via get_task() C2TaskStatus.command."""

    def test_get_task_returns_command(self, adapter):
        payload = {
            "data": {
                "task": [
                    {
                        "display_id": 7,
                        "command_name": "shell",
                        "status": "completed",
                        "completed": True,
                        "timestamp": "2026-06-10T12:00:00Z",
                    }
                ]
            }
        }
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            status = adapter.get_task(7)

        assert status.command == "shell"

    def test_get_task_command_none_when_missing(self, adapter):
        payload = {
            "data": {
                "task": [
                    {
                        "display_id": 7,
                        "command_name": None,
                        "status": "submitted",
                        "completed": False,
                        "timestamp": None,
                    }
                ]
            }
        }
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            status = adapter.get_task(7)

        assert status.command is None
