"""MythicAdapter M3 tests — get_task and get_task_output, mocked HTTP."""
from __future__ import annotations

import pytest
import requests
import requests_mock as rm_module

from backend.app.services.c2.adapter import C2Error
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)


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

        assert status.display_id == 7
        assert status.status == "processing"
        assert status.completed is False
        assert status.completed_at is None

    def test_returns_completed_at_for_completed_task(self, adapter):
        payload = {
            "data": {
                "task": [
                    {
                        "display_id": 7,
                        "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.completed is True
        assert status.completed_at is not None
        assert status.completed_at.year == 2026

    def test_raises_when_task_not_found(self, adapter):
        payload = {"data": {"task": []}}
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            with pytest.raises(C2Error, match="not found"):
                adapter.get_task(999)

    def test_sends_apitoken_header(self, adapter):
        payload = {
            "data": {
                "task": [
                    {"display_id": 1, "status": "submitted", "completed": False, "timestamp": None}
                ]
            }
        }
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            adapter.get_task(1)
            assert m.last_request.headers.get("apitoken") == _TOKEN

    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.get_task(1)

    def test_no_redirect_followed(self, adapter):
        """get_task must not follow HTTP redirects."""
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, status_code=301, headers={"Location": "https://evil.example/"})
            with pytest.raises(C2Error):
                adapter.get_task(1)
            assert len(m.request_history) == 1

    def test_invalid_timestamp_does_not_crash(self, adapter):
        """A malformed timestamp field falls back to completed_at=None without raising."""
        payload = {
            "data": {
                "task": [
                    {
                        "display_id": 5,
                        "status": "completed",
                        "completed": True,
                        "timestamp": "not-a-date",
                    }
                ]
            }
        }
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            status = adapter.get_task(5)

        assert status.completed is True
        assert status.completed_at is None


class TestMythicAdapterGetTaskOutput:
    def test_returns_decoded_output(self, adapter):
        import base64
        encoded = base64.b64encode(b"Administrator\r\n").decode()
        payload = {
            "data": {
                "response": [{"response_text": encoded}]
            }
        }
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            output = adapter.get_task_output(7)

        assert "Administrator" in output

    def test_concatenates_multiple_responses(self, adapter):
        import base64
        r1 = base64.b64encode(b"line one\n").decode()
        r2 = base64.b64encode(b"line two\n").decode()
        payload = {
            "data": {
                "response": [{"response_text": r1}, {"response_text": r2}]
            }
        }
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            output = adapter.get_task_output(7)

        assert "line one" in output
        assert "line two" in output

    def test_returns_empty_string_when_no_responses(self, adapter):
        payload = {"data": {"response": []}}
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            output = adapter.get_task_output(7)

        assert output == ""

    def test_skips_empty_response_text(self, adapter):
        import base64
        encoded = base64.b64encode(b"real output").decode()
        payload = {
            "data": {
                "response": [
                    {"response_text": ""},
                    {"response_text": encoded},
                ]
            }
        }
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, json=payload)
            output = adapter.get_task_output(7)

        assert output == "real output"

    def test_network_error_raises_c2error(self, adapter):
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, exc=requests.exceptions.Timeout("timeout"))
            with pytest.raises(C2Error):
                adapter.get_task_output(7)

    def test_no_redirect_followed(self, adapter):
        with rm_module.Mocker() as m:
            m.post(_GQL_URL, status_code=302, headers={"Location": "https://evil.example/"})
            with pytest.raises(C2Error):
                adapter.get_task_output(1)
            assert len(m.request_history) == 1
