"""Tests for GET /api/simulations/<id>/c2/tasks — poll-on-read endpoint."""
from __future__ import annotations

import pytest
from cryptography.fernet import Fernet
from flask import Flask
from flask.testing import FlaskClient

from backend.app.extensions import db
from backend.app.models.c2_task import C2Task
from backend.app.models.simulation import Simulation
from backend.app.services.c2.adapter import C2Error, C2TaskStatus
from backend.tests.conftest import auth_headers as _h

_FERNET_KEY = Fernet.generate_key().decode()


@pytest.fixture(autouse=True)
def set_encryption_key(monkeypatch):
    monkeypatch.setenv("MIMIC_ENCRYPTION_KEY", _FERNET_KEY)


@pytest.fixture(autouse=True)
def use_fake_adapter(monkeypatch):
    monkeypatch.setenv("MIMIC_C2_ADAPTER", "fake")


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_engagement(client: FlaskClient, token: str) -> dict:
    resp = client.post(
        "/api/engagements",
        headers=_h(token),
        json={"name": "Op Alpha", "start_date": "2026-06-10"},
    )
    assert resp.status_code == 201
    return resp.get_json()


def _put_config(client: FlaskClient, token: str, eid: int) -> None:
    resp = client.put(
        f"/api/engagements/{eid}/c2-config",
        headers=_h(token),
        json={"url": "https://c2.internal:7443", "api_token": "s3cr3t", "verify_tls": True},
    )
    assert resp.status_code == 200


def _make_sim(client: FlaskClient, token: str, eid: int) -> dict:
    resp = client.post(
        f"/api/engagements/{eid}/simulations",
        headers=_h(token),
        json={"name": "Sim Alpha"},
    )
    assert resp.status_code == 201
    return resp.get_json()


def _execute(client: FlaskClient, token: str, sid: int, commands: list, callback_display_id: int = 1):
    return client.post(
        f"/api/simulations/{sid}/c2/execute",
        headers=_h(token),
        json={"callback_display_id": callback_display_id, "commands": commands},
    )


def _list_tasks(client: FlaskClient, token: str, sid: int):
    return client.get(
        f"/api/simulations/{sid}/c2/tasks",
        headers=_h(token),
    )


# ---------------------------------------------------------------------------
# Happy path
# ---------------------------------------------------------------------------


class TestListTasksHappyPath:
    def test_returns_empty_list_when_no_tasks(
        self, client: FlaskClient, admin_token: str
    ) -> None:
        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])

        resp = _list_tasks(client, admin_token, sim["id"])
        assert resp.status_code == 200
        assert resp.get_json()["tasks"] == []

    def test_returns_task_after_execute(
        self, client: FlaskClient, admin_token: str
    ) -> None:
        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        resp = _list_tasks(client, admin_token, sim["id"])
        assert resp.status_code == 200
        tasks = resp.get_json()["tasks"]
        assert len(tasks) == 1
        assert tasks[0]["command"] == "whoami"

    def test_task_shape(
        self, client: FlaskClient, admin_token: str
    ) -> None:
        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["hostname"])

        resp = _list_tasks(client, admin_token, sim["id"])
        task = resp.get_json()["tasks"][0]
        for field in ("id", "mythic_task_display_id", "callback_display_id",
                      "command", "params", "status", "completed", "output",
                      "source", "mapping_applied", "created_at", "completed_at"):
            assert field in task, f"missing field: {field}"
        assert task["source"] == "mimic"

    def test_first_poll_returns_submitted(
        self, client: FlaskClient, admin_token: str
    ) -> None:
        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        # First GET — FakeAdapter.get_task() first call → submitted.
        resp = _list_tasks(client, admin_token, sim["id"])
        task = resp.get_json()["tasks"][0]
        assert task["status"] == "submitted"
        assert task["completed"] is False

    def test_poll_marks_completed_when_adapter_returns_completed(
        self, app: Flask, monkeypatch, client: FlaskClient, admin_token: str
    ) -> None:
        """When adapter.get_task returns completed=True the task is updated in DB."""
        from datetime import UTC, datetime

        from backend.app.services.c2 import fake as fake_mod

        def _completed(self, task_display_id: int) -> C2TaskStatus:
            return C2TaskStatus(
                display_id=task_display_id,
                status="completed",
                completed=True,
                completed_at=datetime.now(UTC),
            )

        monkeypatch.setattr(fake_mod.FakeAdapter, "get_task", _completed)

        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        resp = _list_tasks(client, admin_token, sim["id"])
        task = resp.get_json()["tasks"][0]
        assert task["completed"] is True
        assert task["status"] == "completed"

    def test_output_populated_after_completion(
        self, monkeypatch, client: FlaskClient, admin_token: str
    ) -> None:
        """Output is fetched and stored when task transitions to completed."""
        from datetime import UTC, datetime

        from backend.app.services.c2 import fake as fake_mod

        def _completed(self, task_display_id: int) -> C2TaskStatus:
            return C2TaskStatus(
                display_id=task_display_id,
                status="completed",
                completed=True,
                completed_at=datetime.now(UTC),
            )

        def _output(self, task_display_id: int) -> str:
            return f"whoami result for task {task_display_id}"

        monkeypatch.setattr(fake_mod.FakeAdapter, "get_task", _completed)
        monkeypatch.setattr(fake_mod.FakeAdapter, "get_task_output", _output)

        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        resp = _list_tasks(client, admin_token, sim["id"])
        task = resp.get_json()["tasks"][0]
        assert task["output"] is not None
        assert "whoami" in task["output"]

    def test_mapping_applied_set_after_completion(
        self, app: Flask, monkeypatch, client: FlaskClient, admin_token: str
    ) -> None:
        from datetime import UTC, datetime

        from backend.app.services.c2 import fake as fake_mod

        def _completed(self, task_display_id: int) -> C2TaskStatus:
            return C2TaskStatus(
                display_id=task_display_id,
                status="completed",
                completed=True,
                completed_at=datetime.now(UTC),
            )

        monkeypatch.setattr(fake_mod.FakeAdapter, "get_task", _completed)

        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        _list_tasks(client, admin_token, sim["id"])

        with app.app_context():
            task = C2Task.query.filter_by(simulation_id=sim["id"]).first()
            assert task is not None
            assert task.mapping_applied is True

    def test_execution_result_updated_on_simulation(
        self, app: Flask, monkeypatch, client: FlaskClient, admin_token: str
    ) -> None:
        from datetime import UTC, datetime

        from backend.app.services.c2 import fake as fake_mod

        def _completed(self, task_display_id: int) -> C2TaskStatus:
            return C2TaskStatus(
                display_id=task_display_id,
                status="completed",
                completed=True,
                completed_at=datetime.now(UTC),
            )

        def _output(self, task_display_id: int) -> str:
            return f"WORKSTATION-01\\whoami output {task_display_id}"

        monkeypatch.setattr(fake_mod.FakeAdapter, "get_task", _completed)
        monkeypatch.setattr(fake_mod.FakeAdapter, "get_task_output", _output)

        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        _list_tasks(client, admin_token, sim["id"])

        with app.app_context():
            updated_sim = db.session.get(Simulation, sim["id"])
            assert updated_sim is not None
            assert updated_sim.execution_result is not None
            assert "whoami" in updated_sim.execution_result

    def test_completed_task_not_re_polled(
        self, app: Flask, monkeypatch, client: FlaskClient, admin_token: str
    ) -> None:
        """Once task.completed=True in DB, subsequent GETs skip polling (no re-poll)."""
        from datetime import UTC, datetime

        from backend.app.services.c2 import fake as fake_mod

        call_count = {"n": 0}

        def _completed(self, task_display_id: int) -> C2TaskStatus:
            call_count["n"] += 1
            return C2TaskStatus(
                display_id=task_display_id,
                status="completed",
                completed=True,
                completed_at=datetime.now(UTC),
            )

        monkeypatch.setattr(fake_mod.FakeAdapter, "get_task", _completed)

        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        _list_tasks(client, admin_token, sim["id"])  # 1st GET — marks task completed (1 call)
        first_count = call_count["n"]

        _list_tasks(client, admin_token, sim["id"])  # 2nd GET — task already completed, skip poll

        # get_task should NOT have been called again on the 2nd GET.
        assert call_count["n"] == first_count, "completed task should not be re-polled"

        resp = _list_tasks(client, admin_token, sim["id"])
        assert resp.status_code == 200
        task = resp.get_json()["tasks"][0]
        assert task["completed"] is True

    def test_redteam_can_list_tasks(
        self, client: FlaskClient, admin_token: str, redteam_token: str
    ) -> None:
        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        resp = _list_tasks(client, redteam_token, sim["id"])
        assert resp.status_code == 200


# ---------------------------------------------------------------------------
# Error cases
# ---------------------------------------------------------------------------


class TestListTasksErrors:
    def test_404_simulation_not_found(
        self, client: FlaskClient, admin_token: str
    ) -> None:
        resp = _list_tasks(client, admin_token, 9999)
        assert resp.status_code == 404

    def test_403_soc_forbidden(
        self, client: FlaskClient, admin_token: str, soc_token: str
    ) -> None:
        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])

        resp = _list_tasks(client, soc_token, sim["id"])
        assert resp.status_code == 403

    def test_503_no_encryption_key(
        self, monkeypatch, client: FlaskClient, admin_token: str
    ) -> None:
        monkeypatch.delenv("MIMIC_ENCRYPTION_KEY", raising=False)
        eng = _make_engagement(client, admin_token)
        sim = _make_sim(client, admin_token, eng["id"])

        resp = _list_tasks(client, admin_token, sim["id"])
        assert resp.status_code == 503

    def test_404_no_c2_config(
        self, client: FlaskClient, admin_token: str
    ) -> None:
        eng = _make_engagement(client, admin_token)
        sim = _make_sim(client, admin_token, eng["id"])

        resp = _list_tasks(client, admin_token, sim["id"])
        assert resp.status_code == 404

    def test_adapter_error_during_poll_is_tolerated(
        self, monkeypatch, client: FlaskClient, admin_token: str
    ) -> None:
        """If get_task raises C2Error during poll, the task is skipped (best-effort)."""
        from backend.app.services.c2 import fake as fake_mod

        def _boom(self, task_display_id: int):
            raise C2Error("upstream unavailable")

        monkeypatch.setattr(fake_mod.FakeAdapter, "get_task", _boom)

        eng = _make_engagement(client, admin_token)
        _put_config(client, admin_token, eng["id"])
        sim = _make_sim(client, admin_token, eng["id"])
        _execute(client, admin_token, sim["id"], ["whoami"])

        # Should still return 200 with the task (un-refreshed status).
        resp = _list_tasks(client, admin_token, sim["id"])
        assert resp.status_code == 200
        tasks = resp.get_json()["tasks"]
        assert len(tasks) == 1
        # Status is stale (not updated due to error) — still "submitted".
        assert tasks[0]["status"] == "submitted"
