"""Tests for mcpaudit flows — toxic data flow analysis.

SECURITY NOTE: This test file contains string literals that represent
vulnerable code patterns. These strings are ONLY parsed as AST by the
FlowAnalyzer static analysis engine — they are NEVER executed.
The purpose is to verify the analyzer correctly detects dangerous patterns.
"""

from __future__ import annotations

import json
import textwrap

import pytest

from mcpaudit.flows import (
    FlowAnalyzer,
    FlowFinding,
    FlowPattern,
    FlowResult,
    _get_all_flow_patterns,
)
from mcpaudit.models import Severity


def _analyzer() -> FlowAnalyzer:
    return FlowAnalyzer()


# All test code strings below are static analysis targets, not executed code.

class TestFlow001:
    """FLOW-001: User input reaching command execution sinks."""

    def test_input_to_subprocess(self):
        code = 'import subprocess\ncmd = input("x")\nsubprocess.run(cmd, shell=True)'
        result = _analyzer().analyze_source(code, "s.py")
        assert any(f.pattern_id == "FLOW-001" for f in result.flows)

    def test_request_args_detected(self):
        code = 'import os\npath = request.args.get("p")\nos.system(f"ls {path}")'
        result = _analyzer().analyze_source(code, "s.py")
        assert any(f.pattern_id == "FLOW-001" for f in result.flows)


class TestFlow002:
    """FLOW-002: Tool results interpolated into prompts."""

    def test_tool_result_fstring(self):
        code = 'result = call_tool("search", q)\nprompt = f"Based on {result}"'
        result = _analyzer().analyze_source(code, "s.py")
        assert any(f.pattern_id == "FLOW-002" for f in result.flows)


class TestFlow003:
    """FLOW-003: File data reaching compile() sinks."""

    def test_file_to_compile(self):
        code = 'content = path.read_text()\ncode = compile(content, "m", "exec")'
        result = _analyzer().analyze_source(code, "s.py")
        assert any(f.pattern_id == "FLOW-003" for f in result.flows)


class TestFlow004:
    """FLOW-004: Env vars reaching command execution sinks."""

    def test_getenv_to_subprocess(self):
        code = 'import os, subprocess\ncmd = os.getenv("X")\nsubprocess.run(cmd, shell=True)'
        result = _analyzer().analyze_source(code, "s.py")
        assert any(f.pattern_id == "FLOW-004" for f in result.flows)

    def test_environ_get_to_system(self):
        code = 'import os\np = os.environ.get("P")\nos.system(p)'
        result = _analyzer().analyze_source(code, "s.py")
        assert any(f.pattern_id == "FLOW-004" for f in result.flows)


class TestFlow006:
    """FLOW-006: Network data reaching code compilation sinks."""

    def test_requests_to_compile(self):
        code = 'import requests\nd = requests.get("http://x")\ncompile(d.text, "m", "exec")'
        result = _analyzer().analyze_source(code, "s.py")
        assert any(f.pattern_id == "FLOW-006" for f in result.flows)

    def test_httpx_to_compile(self):
        code = 'r = httpx.post("http://x")\ncompile(r.text, "m", "exec")'
        result = _analyzer().analyze_source(code, "s.py")
        assert any(f.pattern_id == "FLOW-006" for f in result.flows)


class TestFlowResult:
    def test_empty(self):
        r = FlowResult(file="t.py")
        assert r.total_flows == 0
        assert r.exit_code == 0
        assert not r.has_critical
        assert not r.has_high

    def test_critical_exit_code(self):
        r = FlowResult(file="t.py", flows=[
            FlowFinding("FLOW-001", Severity.CRITICAL, "t", "d", "f", 1, 2)])
        assert r.exit_code == 2
        assert r.has_critical

    def test_high_exit_code(self):
        r = FlowResult(file="t.py", flows=[
            FlowFinding("FLOW-002", Severity.HIGH, "t", "d", "f", 1, 2)])
        assert r.exit_code == 1
        assert r.has_high

    def test_to_dict(self):
        r = FlowResult(file="t.py", files_analyzed=1)
        d = r.to_dict()
        assert d["file"] == "t.py"
        assert d["total_flows"] == 0
        assert isinstance(d["severity_counts"], dict)

    def test_severity_counts(self):
        r = FlowResult(file="t.py", flows=[
            FlowFinding("FLOW-001", Severity.CRITICAL, "a", "b", "f", 1, 2),
            FlowFinding("FLOW-002", Severity.HIGH, "c", "d", "f", 3, 4),
            FlowFinding("FLOW-003", Severity.CRITICAL, "e", "f", "f", 5, 6)])
        assert r.severity_counts["CRITICAL"] == 2
        assert r.severity_counts["HIGH"] == 1


class TestFlowFinding:
    def test_to_dict(self):
        ff = FlowFinding("FLOW-001", Severity.CRITICAL, "title", "desc",
                         "s.py", 5, 10, "src", "sink", ["a", "b"], "CWE-78")
        d = ff.to_dict()
        assert d["pattern_id"] == "FLOW-001"
        assert d["source_line"] == 5
        assert len(d["flow_path"]) == 2

    def test_to_finding(self):
        ff = FlowFinding("FLOW-001", Severity.CRITICAL, "T", "D",
                         "s.py", 1, 5, flow_path=["a", "b"], cwe="CWE-78")
        f = ff.to_finding()
        assert f.rule_id == "FLOW-001"
        assert "Flow:" in f.description


class TestFlowPattern:
    def test_all_six_patterns(self):
        p = _get_all_flow_patterns()
        assert len(p) == 6
        assert {x.pattern_id for x in p} == {f"FLOW-00{i}" for i in range(1, 7)}

    def test_to_dict(self):
        for p in _get_all_flow_patterns():
            d = p.to_dict()
            assert "pattern_id" in d
            assert "severity" in d

    def test_all_have_cwe(self):
        for p in _get_all_flow_patterns():
            assert p.cwe, f"{p.pattern_id} missing CWE"


class TestAnalyzerInit:
    def test_defaults(self):
        a = FlowAnalyzer()
        assert len(a.patterns) == 6

    def test_custom(self):
        a = FlowAnalyzer([FlowPattern("X", "x", "x", Severity.LOW, "a", "b")])
        assert len(a.patterns) == 1

    def test_empty_source(self):
        r = _analyzer().analyze_source("", "e.py")
        assert r.total_flows == 0

    def test_syntax_error(self):
        r = _analyzer().analyze_source("def foo(", "b.py")
        assert len(r.parse_errors) > 0


class TestEdgeCases:
    def test_no_sources_no_sinks(self):
        r = _analyzer().analyze_source("x = 1 + 2\nprint(x)", "safe.py")
        assert r.total_flows == 0

    def test_json_roundtrip(self):
        r = FlowResult(file="t.py", files_analyzed=1, flows=[
            FlowFinding("FLOW-001", Severity.CRITICAL, "t", "d", "f", 1, 2)])
        parsed = json.loads(json.dumps(r.to_dict()))
        assert parsed["total_flows"] == 1

    def test_multiple_flows(self):
        code = 'import subprocess, os\ncmd = input("x")\nsubprocess.run(cmd)\np = os.getenv("P")\nos.system(p)'
        r = _analyzer().analyze_source(code, "s.py")
        assert r.total_flows >= 2
