"""
Tests for the agentic controller enforcement layers in agentic_controller_v2.py.

Covers:
- Verification result checking (not just presence)
- Write-gate enforcement
- AST/structural validation (multi-language)
- Semantic risk detection
- ApplyPatch diff reconstruction
- StrReplace content reconstruction
- Import/undefined-reference checking
- Verification failure pattern detection
"""

import pytest

from api.services.complete.agentic_controller_v2 import (
    _check_verification_result,
    _check_structural_validity,
    _validate_python_ast,
    _validate_ts_js_structure,
    _validate_go_structure,
    _validate_bracket_balance,
    _validate_json,
    _detect_semantic_risk,
    _reconstruct_after_str_replace,
    _reconstruct_after_apply_patch,
    _apply_unified_diff,
    _check_undefined_references,
    _check_ts_undefined,
    _check_py_undefined,
    looks_like_unbacked_tool_claim,
    add_tool_honesty_system_message,
)
from api.models import CompleteRequest


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

def _msg(role, content, tool_calls=None, tool_call_id=None):
    kwargs = {"role": role, "content": content}
    if tool_calls is not None:
        kwargs["tool_calls"] = tool_calls
    if tool_call_id is not None:
        kwargs["tool_call_id"] = tool_call_id
    return CompleteRequest.Message(**kwargs)


def _tc(name, arguments, tc_id="tc-1"):
    return {"id": tc_id, "function": {"name": name, "arguments": arguments}}


# ===========================================================================
# Verification result checking
# ===========================================================================

class TestVerificationResultChecking:
    def test_readlints_clean_returns_none(self):
        assert _check_verification_result("No diagnostics found.", "readlints") is None

    def test_readlints_zero_errors_returns_none(self):
        assert _check_verification_result("0 errors, 0 warnings", "readlints") is None

    def test_readlints_with_errors_returns_failure(self):
        output = "src/app.ts:10:5 error TS2322: Type 'string' is not assignable to type 'number'."
        result = _check_verification_result(output, "readlints")
        assert result is not None
        assert "error" in result.lower()

    def test_readlints_multiple_errors(self):
        output = (
            "src/a.ts:1:1 error TS1005: ';' expected.\n"
            "src/b.ts:5:3 error TS2304: Cannot find name 'foo'.\n"
            "src/c.ts:10:1 error TS1005: '}' expected."
        )
        result = _check_verification_result(output, "readlints")
        assert result is not None
        assert "3 error" in result

    def test_shell_exit_code_zero_returns_none(self):
        assert _check_verification_result("All tests passed. exit code: 0", "shell") is None

    def test_shell_exit_code_nonzero_returns_failure(self):
        result = _check_verification_result(
            "FAILED test_foo.py::test_bar\nexit code: 1", "shell"
        )
        assert result is not None
        assert "code 1" in result

    def test_shell_test_failure_without_exit_code(self):
        result = _check_verification_result(
            "FAILED tests/test_foo.py::test_bar - AssertionError", "shell"
        )
        assert result is not None
        assert "error" in result.lower() or "FAILED" in result

    def test_shell_success_output_returns_none(self):
        assert _check_verification_result("All checks passed in 2.3s", "shell") is None

    def test_empty_output_returns_none(self):
        assert _check_verification_result("", "readlints") is None
        assert _check_verification_result("  ", "shell") is None

    def test_unknown_tool_type_returns_none(self):
        assert _check_verification_result("error error error", "unknown") is None

    def test_shell_traceback_detected(self):
        output = (
            "Traceback (most recent call last):\n"
            "  File 'test.py', line 5\n"
            "    raise ValueError('bad')\n"
            "ValueError: bad"
        )
        result = _check_verification_result(output, "shell")
        assert result is not None

    def test_shell_clean_with_success_keyword(self):
        assert _check_verification_result("Build success", "shell") is None


# ===========================================================================
# Multi-language structural validation
# ===========================================================================

class TestStructuralValidation:
    def test_valid_python(self):
        assert _validate_python_ast("x = 1\ndef foo():\n    return x\n") is None

    def test_invalid_python(self):
        result = _validate_python_ast("def foo(\n    return")
        assert result is not None
        assert "syntax" in result.lower() or "parse" in result.lower()

    def test_valid_typescript(self):
        code = "import { foo } from 'bar';\nconst x = foo();\nexport default x;\n"
        assert _validate_ts_js_structure(code) is None

    def test_unbalanced_typescript(self):
        code = "function foo() {\n  if (true) {\n    return 1;\n}\n"
        result = _validate_ts_js_structure(code)
        assert result is not None
        assert "unclosed" in result.lower() or "unmatched" in result.lower()

    def test_valid_go(self):
        code = "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n"
        assert _validate_go_structure(code) is None

    def test_go_missing_package(self):
        code = "import \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"hello\")\n}\n"
        result = _validate_go_structure(code)
        assert result is not None
        assert "package" in result.lower()

    def test_go_comment_before_package_is_ok(self):
        code = "// Package main does stuff.\npackage main\n\nfunc main() {}\n"
        assert _validate_go_structure(code) is None

    def test_valid_json(self):
        assert _validate_json('{"key": "value", "num": 42}') is None

    def test_invalid_json(self):
        result = _validate_json('{"key": "value",}')
        assert result is not None
        assert "json" in result.lower()

    def test_bracket_balance_valid(self):
        assert _validate_bracket_balance("fn main() { let x = [1, 2]; }", "Rust") is None

    def test_bracket_balance_unmatched_close(self):
        result = _validate_bracket_balance("fn main() { } }", "Rust")
        assert result is not None
        assert "unmatched" in result.lower()

    def test_bracket_balance_ignores_strings(self):
        code = 'let s = "this has { unbalanced";'
        assert _validate_bracket_balance(code, "JS") is None

    def test_bracket_balance_ignores_comments(self):
        code = "fn main() {\n  // { not a real bracket\n}\n"
        assert _validate_bracket_balance(code, "Rust") is None

    def test_check_structural_validity_dispatches_by_extension(self):
        assert _check_structural_validity("x = 1", "foo.py") is None
        assert _check_structural_validity("package main\nfunc main() {}", "foo.go") is None
        assert _check_structural_validity('{"a": 1}', "foo.json") is None
        assert _check_structural_validity("const x = 1;", "foo.ts") is None
        assert _check_structural_validity("anything", "foo.txt") is None  # unsupported ext

    def test_mismatched_brackets(self):
        code = "function foo() { return [1, 2); }"
        result = _validate_bracket_balance(code, "JS")
        assert result is not None
        assert "mismatched" in result.lower()


# ===========================================================================
# Semantic risk detection
# ===========================================================================

class TestSemanticRisk:
    def test_auth_stripping_detected(self):
        old = "@require_auth\ndef delete_user(): pass"
        new = "def delete_user(): pass"
        result = _detect_semantic_risk(old, new)
        assert result is not None
        assert "require_auth" in result

    def test_error_swallowing_detected(self):
        old = "try:\n    do_stuff()\nexcept Exception as e:\n    log(e)"
        new = "try:\n    do_stuff()\nexcept Exception: pass"
        result = _detect_semantic_risk(old, new)
        assert result is not None
        assert "swallowing" in result.lower()

    def test_safe_change_passes(self):
        old = "x = 1\ny = 2"
        new = "x = 1\ny = 3\nz = 4"
        assert _detect_semantic_risk(old, new) is None

    def test_guard_clause_removal_detected(self):
        old = "if not valid:\n    raise ValueError('bad')\nprocess()"
        new = "process()"
        result = _detect_semantic_risk(old, new)
        assert result is not None
        assert "guard" in result.lower() or "raise" in result.lower()

    def test_config_removal_detected(self):
        old = "if feature_flags.is_enabled('new_ui'):\n    show_new_ui()"
        new = "show_new_ui()"
        result = _detect_semantic_risk(old, new)
        assert result is not None
        assert "feature_flags" in result


# ===========================================================================
# StrReplace reconstruction
# ===========================================================================

class TestStrReplaceReconstruction:
    def test_basic_replace(self):
        result = _reconstruct_after_str_replace("hello world", "world", "earth")
        assert result == "hello earth"

    def test_replace_all(self):
        result = _reconstruct_after_str_replace("aaa", "a", "b", replace_all=True)
        assert result == "bbb"

    def test_replace_first_only(self):
        result = _reconstruct_after_str_replace("aaa", "a", "b", replace_all=False)
        assert result == "baa"

    def test_old_string_not_found(self):
        assert _reconstruct_after_str_replace("hello", "xyz", "abc") is None

    def test_empty_inputs(self):
        assert _reconstruct_after_str_replace("", "a", "b") is None
        assert _reconstruct_after_str_replace("hello", "", "b") is None


# ===========================================================================
# ApplyPatch diff reconstruction
# ===========================================================================

class TestApplyPatchReconstruction:
    def test_basic_unified_diff(self):
        original = "line1\nline2\nline3\n"
        patch = (
            "--- a/test.py\n"
            "+++ b/test.py\n"
            "@@ -1,3 +1,3 @@\n"
            " line1\n"
            "-line2\n"
            "+line2_modified\n"
            " line3\n"
        )
        read_texts = {"test.py": original}
        path, result = _reconstruct_after_apply_patch(patch, read_texts)
        assert path == "test.py"
        assert result is not None
        assert "line2_modified" in result
        assert "line2\n" not in result

    def test_addition_only(self):
        original = "line1\nline3\n"
        patch = (
            "--- a/test.py\n"
            "+++ b/test.py\n"
            "@@ -1,2 +1,3 @@\n"
            " line1\n"
            "+line2\n"
            " line3\n"
        )
        read_texts = {"test.py": original}
        path, result = _reconstruct_after_apply_patch(patch, read_texts)
        assert result is not None
        assert "line2\n" in result

    def test_deletion_only(self):
        original = "line1\nline2\nline3\n"
        patch = (
            "--- a/test.py\n"
            "+++ b/test.py\n"
            "@@ -1,3 +1,2 @@\n"
            " line1\n"
            "-line2\n"
            " line3\n"
        )
        read_texts = {"test.py": original}
        path, result = _reconstruct_after_apply_patch(patch, read_texts)
        assert result is not None
        assert "line2" not in result

    def test_no_matching_file_returns_none(self):
        patch = "--- a/missing.py\n+++ b/missing.py\n@@ -1 +1 @@\n-old\n+new\n"
        path, result = _reconstruct_after_apply_patch(patch, {})
        assert path == "missing.py"
        assert result is None

    def test_empty_patch(self):
        path, result = _reconstruct_after_apply_patch("", {})
        assert path == ""
        assert result is None

    def test_apply_unified_diff_basic(self):
        original = "a\nb\nc\n"
        patch = "@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n"
        result = _apply_unified_diff(original, patch)
        assert result is not None
        assert "B\n" in result
        assert "b\n" not in result


# ===========================================================================
# Import / undefined reference checking
# ===========================================================================

class TestUndefinedReferences:
    def test_ts_missing_import(self):
        code = "const x = <MyComponent />;"
        result = _check_ts_undefined(code)
        assert result is not None
        assert "MyComponent" in result

    def test_ts_imported_component_ok(self):
        code = "import { MyComponent } from './components';\nconst x = <MyComponent />;"
        assert _check_ts_undefined(code) is None

    def test_ts_locally_defined_ok(self):
        code = "function MyComponent() { return <div />; }\nconst x = <MyComponent />;"
        assert _check_ts_undefined(code) is None

    def test_ts_react_builtins_ok(self):
        code = "const x = <React.Fragment><Suspense fallback={null} /></React.Fragment>;"
        assert _check_ts_undefined(code) is None

    def test_py_missing_decorator(self):
        code = "@my_custom_decorator\ndef foo(): pass"
        result = _check_py_undefined(code)
        assert result is not None
        assert "my_custom_decorator" in result

    def test_py_imported_decorator_ok(self):
        code = "from mylib import my_custom_decorator\n\n@my_custom_decorator\ndef foo(): pass"
        assert _check_py_undefined(code) is None

    def test_py_builtin_decorators_ok(self):
        code = "@property\ndef foo(self): return self._foo\n\n@staticmethod\ndef bar(): pass"
        assert _check_py_undefined(code) is None

    def test_py_locally_defined_decorator_ok(self):
        code = "def my_deco(fn): return fn\n\n@my_deco\ndef foo(): pass"
        assert _check_py_undefined(code) is None

    def test_dispatch_by_extension(self):
        assert _check_undefined_references("x = 1", "test.py") is None
        assert _check_undefined_references("const x = 1;", "test.ts") is None
        assert _check_undefined_references("anything", "test.txt") is None


# ===========================================================================
# Tool honesty / unbacked claims
# ===========================================================================

class TestToolHonesty:
    def test_unbacked_grep_claim(self):
        msgs = [
            _msg("user", "What does this codebase do?"),
            _msg("assistant", "The grep output shows that the main module is in backend/api."),
        ]
        assert looks_like_unbacked_tool_claim(msgs) is True

    def test_legitimate_tool_use_not_flagged(self):
        msgs = [
            _msg("user", "Search for foo"),
            _msg("assistant", "Let me search.", tool_calls=[_tc("Grep", '{"pattern": "foo"}')]),
            _msg("tool", "Found foo in bar.py", tool_call_id="tc-1"),
            _msg("assistant", "I found foo in bar.py based on the grep results."),
        ]
        assert looks_like_unbacked_tool_claim(msgs) is False

    def test_no_claims_not_flagged(self):
        msgs = [
            _msg("user", "Hello"),
            _msg("assistant", "Hi! How can I help?"),
        ]
        assert looks_like_unbacked_tool_claim(msgs) is False

    def test_add_tool_honesty_message(self):
        msgs = [_msg("user", "Hello")]
        result = add_tool_honesty_system_message(msgs)
        assert len(result) >= len(msgs)
