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

import csv as _csv
import io as _io
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"),
        "description": "Execute a script",
        "commands": "whoami",
        "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)


# ---------------------------------------------------------------------------
# Shared constants
# ---------------------------------------------------------------------------

_FR_HEADERS = [
    "Scénario",
    "Test",
    "Source de log",
    "Commentaires SOC",
    "Exécution",
    "Logs remontés au SIEM",
    "Cyber incident",
]


# ---------------------------------------------------------------------------
# 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_has_seven_column_table_headers(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim()
        result = render_engagement_markdown(eng, [sim])
        for header in _FR_HEADERS:
            assert header in result, f"Expected French header '{header}' in markdown table"


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_no_simulations_has_no_table(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        result = render_engagement_markdown(eng, [])
        assert "Scénario" not in result
        assert "## Simulations" not in result


def test_render_engagement_markdown_execution_cell_uses_br_separator(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(
            executed_at=datetime(2026, 6, 1, 10, 0, tzinfo=UTC),
            commands="whoami",
            execution_result="admin@host",
        )
        result = render_engagement_markdown(eng, [sim])
        assert "<br/>" in result
        assert "whoami" in result
        assert "admin@host" in result


def test_render_engagement_markdown_pipe_in_cell_is_escaped(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(name="Name | with pipe")
        result = render_engagement_markdown(eng, [sim])
        assert "Name \\| with pipe" in result


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


def _parse_csv(csv_text: str) -> list[list[str]]:
    return list(_csv.reader(_io.StringIO(csv_text)))


def test_render_engagement_csv_has_header_row(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        result = render_engagement_csv(eng, [])
        rows = _parse_csv(result)
        assert rows[0] == _FR_HEADERS


def test_render_engagement_csv_has_one_row_per_simulation(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sims = [_make_sim(1, "S1"), _make_sim(2, "S2")]
        result = render_engagement_csv(eng, sims)
        rows = _parse_csv(result)
        assert len(rows) == 3  # header + 2 sims


def test_render_engagement_csv_columns_are_seven(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim()
        result = render_engagement_csv(eng, [sim])
        rows = _parse_csv(result)
        assert len(rows[0]) == 7
        assert len(rows[1]) == 7


def test_render_engagement_csv_execution_column_contains_commands(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(
            executed_at=datetime(2026, 6, 1, 10, 0, tzinfo=UTC),
            commands="net user /domain",
            execution_result="success",
        )
        result = render_engagement_csv(eng, [sim])
        rows = _parse_csv(result)
        exec_cell = rows[1][4]  # col index 4 = Exécution
        assert "2026-06-01" in exec_cell
        assert "net user /domain" in exec_cell
        assert "success" in exec_cell


# ---------------------------------------------------------------------------
# CSV formula injection defense
# ---------------------------------------------------------------------------


def _parse_csv_data_row(csv_text: str, row_index: int = 1) -> list[str]:
    return _parse_csv(csv_text)[row_index]


def test_render_engagement_csv_escapes_formula_injection_in_scenario(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)
        # col 0 = Scénario
        assert cells[0] == "'=cmd|'/c calc'!A1"


def test_render_engagement_csv_escapes_formula_injection_in_execution(app) -> None:
    with app.app_context():
        eng = _make_engagement()
        # executed_at=None so concat is "\ncommand\n" — leading \n is not a trigger.
        # Use a formula-triggering execution_result to test the final concat.
        sim = _make_sim(execution_result="=HYPERLINK(\"http://evil\")")
        result = render_engagement_csv(eng, [sim])
        cells = _parse_csv_data_row(result)
        # col 4 = Exécution; concat starts with "\n" (empty executed_at) so not triggered
        # but the execution_result value is embedded — verify it's present
        assert "HYPERLINK" in cells[4]


def test_render_engagement_csv_defuses_formula_in_inner_execution_lines(app) -> None:
    """When executed_at is set, the cell starts with a safe date, but commands
    line may inject formulas. Each user-controlled component must be defused."""
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(
            executed_at=datetime(2026, 6, 1, 10, 0, tzinfo=UTC),
            commands="=cmd|'/c calc'!A1",
            execution_result="@SUM(1)",
        )
        result = render_engagement_csv(eng, [sim])
        cells = list(_csv.reader(_io.StringIO(result)))[1]
        execution_cell = cells[4]  # Exécution column
        assert "'=cmd|'/c calc'!A1" in execution_cell
        assert "'@SUM(1)" in execution_cell


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[0] == "Mimikatz LSASS Dump"
        assert "whoami /all" in cells[4]


def test_render_engagement_markdown_escapes_html_in_table_cells(app) -> None:
    """User content in table cells must be HTML-escaped to prevent stored XSS
    when the .md is opened in a renderer that interprets inline HTML."""
    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim(
            name="<script>alert(1)</script>",
            commands='<img src=x onerror="alert(1)">',
        )
        result = render_engagement_markdown(eng, [sim])
        assert "<script>" not in result
        assert 'onerror="alert' not in result
        assert "&lt;script&gt;" in result
        assert "&lt;img" in result
        # double-quotes in attribute values are also escaped
        assert "&quot;" in result


# ---------------------------------------------------------------------------
# PDF tests
# ---------------------------------------------------------------------------


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"


def test_render_engagement_pdf_contains_simulation_table(app) -> None:
    from backend.app.services.export import _render_engagement_html

    with app.app_context():
        eng = _make_engagement()
        sim = _make_sim()
        html = _render_engagement_html(eng, [sim])
        assert "<table>" in html
        for header in _FR_HEADERS:
            assert header in html, f"Expected French header '{header}' in HTML"


def test_render_engagement_html_has_landscape_page_rule(app) -> None:
    from backend.app.services.export import _render_engagement_html

    with app.app_context():
        eng = _make_engagement()
        html = _render_engagement_html(eng, [])
        assert "landscape" in html, "HTML must include A4 landscape @page rule for PDF output"


# ---------------------------------------------------------------------------
# Defense-in-depth: filename header injection
# ---------------------------------------------------------------------------


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 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
