"""Unit tests for render functions in backend.app.services.export."""
from __future__ import annotations

from datetime import UTC, datetime
from types import SimpleNamespace
from typing import Any

from backend.app.services.export import (
    render_engagement_csv,
    render_engagement_markdown,
    render_engagement_pdf,
)

# ---------------------------------------------------------------------------
# Fixtures / factories
# ---------------------------------------------------------------------------


def _make_engagement(**kw) -> Any:
    from datetime import date

    defaults: dict[str, Any] = {
        "id": 1,
        "name": "Test Engagement",
        "description": "A purple team exercise",
        "start_date": date(2026, 6, 1),
        "end_date": date(2026, 6, 30),
        "status": SimpleNamespace(value="active"),
        "created_at": datetime(2026, 6, 1, 10, 0, tzinfo=UTC),
        "created_by": SimpleNamespace(username="alice"),
    }
    defaults.update(kw)
    return SimpleNamespace(**defaults)


def _make_sim(sid: int = 1, name: str = "Sim Alpha", **kw) -> Any:
    defaults: dict[str, Any] = {
        "id": sid,
        "name": name,
        "status": SimpleNamespace(value="pending"),
        "techniques": [{"id": "T1059", "name": "Command and Scripting Interpreter"}],
        "tactic_ids": ["TA0002"],
        "description": "Execute a script",
        "commands": "whoami",
        "prerequisites": "local admin",
        "executed_at": None,
        "execution_result": None,
        "log_source": None,
        "logs": None,
        "soc_comment": None,
        "incident_number": None,
        "created_at": datetime(2026, 6, 1, 10, 0, tzinfo=UTC),
        "updated_at": None,
        "created_by": SimpleNamespace(username="bob"),
    }
    defaults.update(kw)
    return SimpleNamespace(**defaults)


# ---------------------------------------------------------------------------
# Markdown tests
# ---------------------------------------------------------------------------


def test_render_engagement_markdown_includes_header_fields(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        result = render_engagement_markdown(eng, [])
        assert "Test Engagement" in result
        assert "2026-06-01" in result
        assert "2026-06-30" in result
        assert "active" in result
        assert "alice" in result


def test_render_engagement_markdown_lists_all_simulations_in_order(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sims = [_make_sim(1, "First Sim"), _make_sim(2, "Second Sim")]
        result = render_engagement_markdown(eng, sims)
        first_pos = result.index("First Sim")
        second_pos = result.index("Second Sim")
        assert first_pos < second_pos


def test_render_engagement_markdown_includes_techniques_with_id_and_name(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(
            techniques=[{"id": "T1059", "name": "Command and Scripting Interpreter"}]
        )
        result = render_engagement_markdown(eng, [sim])
        assert "T1059" in result
        assert "Command and Scripting Interpreter" in result


def test_render_engagement_markdown_includes_tactics(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(tactic_ids=["TA0002"])
        result = render_engagement_markdown(eng, [sim])
        # TA0002 = Execution — should appear as tactic name or id
        assert "TA0002" in result or "Execution" in result


def test_render_engagement_markdown_includes_soc_fields_even_when_blank(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(log_source=None, logs=None, soc_comment=None, incident_number=None)
        result = render_engagement_markdown(eng, [sim])
        assert "Log source" in result
        assert "SOC comment" in result
        assert "Incident number" in result


def test_render_engagement_markdown_escapes_backticks_in_commands(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(commands="echo `whoami`")
        result = render_engagement_markdown(eng, [sim])
        # Commands must be inside a fenced code block using ~~~ (tilde fences)
        assert "~~~" in result
        # The backtick content must still be present
        assert "`whoami`" in result


# ---------------------------------------------------------------------------
# CSV tests
# ---------------------------------------------------------------------------


def test_render_engagement_csv_has_header_row(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        result = render_engagement_csv(eng, [])
        first_line = result.splitlines()[0]
        assert "id" in first_line
        assert "name" in first_line
        assert "status" in first_line
        assert "techniques" in first_line


def test_render_engagement_csv_joins_multi_techniques_with_pipe(app) -> None:
    with app.app_context():
        import csv
        import io

        eng = _make_engagement()
        sim = _make_sim(
            techniques=[
                {"id": "T1059", "name": "Command and Scripting Interpreter"},
                {"id": "T1078", "name": "Valid Accounts"},
            ]
        )
        result = render_engagement_csv(eng, [sim])
        rows = list(csv.reader(io.StringIO(result)))
        # row[3] = techniques column
        tech_cell = rows[1][3]
        assert "|" in tech_cell
        assert "T1059" in tech_cell
        assert "T1078" in tech_cell


# ---------------------------------------------------------------------------
# PDF test
# ---------------------------------------------------------------------------


def test_render_engagement_pdf_starts_with_pdf_magic(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim()
        result = render_engagement_pdf(eng, [sim])
        assert isinstance(result, bytes)
        assert result[:4] == b"%PDF"


# ---------------------------------------------------------------------------
# MITRE bundle not loaded
# ---------------------------------------------------------------------------


def test_render_engagement_markdown_with_mitre_bundle_not_loaded_does_not_crash(
    app,
) -> None:
    with app.app_context():
        import backend.app.services.mitre as mitre_svc

        original = mitre_svc.mitre_loaded
        original_tactics = mitre_svc._tactics_by_technique.copy()
        try:
            mitre_svc.mitre_loaded = False
            mitre_svc._tactics_by_technique = {}
            eng = _make_engagement()
            sim = _make_sim(
                techniques=[{"id": "T1059", "name": "Command and Scripting Interpreter"}],
                tactic_ids=["TA0002"],
            )
            result = render_engagement_markdown(eng, [sim])
            # Must not crash and must include the technique id
            assert "T1059" in result
        finally:
            mitre_svc.mitre_loaded = original
            mitre_svc._tactics_by_technique = original_tactics


# ---------------------------------------------------------------------------
# CSV formula injection defense (security fix — 2026-06-08)
# ---------------------------------------------------------------------------

import csv as _csv  # noqa: E402  (sectioned import to keep test diff localized)
import io as _io  # noqa: E402


def _parse_csv_data_row(csv_text: str, row_index: int = 1) -> list[str]:
    """Return cells of row N (0=header) parsed by csv.reader to handle multilines."""
    return list(_csv.reader(_io.StringIO(csv_text)))[row_index]


def test_render_engagement_csv_escapes_formula_injection_in_name(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(name="=cmd|'/c calc'!A1")
        result = render_engagement_csv(eng, [sim])
        cells = _parse_csv_data_row(result)
        # name is column index 1 (after id)
        assert cells[1] == "'=cmd|'/c calc'!A1", (
            "name beginning with = must be apostrophe-prefixed to defuse Excel formula"
        )


def test_render_engagement_csv_escapes_formula_injection_in_commands(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(commands="@SUM(1+1)")
        result = render_engagement_csv(eng, [sim])
        cells = _parse_csv_data_row(result)
        # commands is column index 6 per _CSV_HEADERS order
        assert cells[6].startswith("'@"), "commands beginning with @ must be apostrophe-prefixed"


def test_render_engagement_csv_does_not_alter_safe_strings(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(name="Mimikatz LSASS Dump", commands="whoami /all")
        result = render_engagement_csv(eng, [sim])
        cells = _parse_csv_data_row(result)
        assert cells[1] == "Mimikatz LSASS Dump", "safe name must not be modified"
        assert cells[6] == "whoami /all", "safe commands must not be modified"


def test_export_filename_never_contains_quote_or_crlf() -> None:
    """Defense-in-depth: even with malicious engagement names, the filename
    used in Content-Disposition must never contain header-injection chars."""
    from types import SimpleNamespace

    from backend.app.services.export import _export_filename
    evil = SimpleNamespace(id=42, name='evil"name\r\nX-Injected: yes')
    fname = _export_filename(evil, "md")
    assert '"' not in fname
    assert '\r' not in fname
    assert '\n' not in fname
