"""Tests for quality checkers (QUAL001-QUAL009)."""

import pytest
from pathlib import Path

from stablestack.checkers.quality.exception_swallowing import ExceptionSwallowingChecker
from stablestack.checkers.quality.mutable_default import MutableDefaultChecker
from stablestack.checkers.quality.broad_exception import BroadExceptionChecker
from stablestack.checkers.quality.file_length import FileLengthChecker
from stablestack.checkers.quality.mixed_boolean_operators import MixedBooleanOperatorsChecker


class TestExceptionSwallowingChecker:
    """Tests for QUAL001: exception-swallowing."""

    @pytest.fixture
    def checker(self):
        return ExceptionSwallowingChecker()

    def test_detects_except_pass(self, checker, temp_py_file):
        """Should detect except: pass pattern."""
        code = '''
try:
    risky_operation()
except:
    pass
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "QUAL001" for f in findings)

    def test_detects_except_exception_pass(self, checker, temp_py_file):
        """Should detect except Exception: pass pattern."""
        code = '''
try:
    risky_operation()
except Exception:
    pass
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1

    def test_allows_logged_exception(self, checker, temp_py_file):
        """Should not flag exception with logging."""
        code = '''
try:
    risky_operation()
except Exception as e:
    logger.error(f"Operation failed: {e}")
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


class TestMutableDefaultChecker:
    """Tests for QUAL004: mutable-default-argument."""

    @pytest.fixture
    def checker(self):
        return MutableDefaultChecker()

    def test_detects_list_default(self, checker, temp_py_file):
        """Should detect list as default argument."""
        code = '''
def add_item(item, items=[]):
    items.append(item)
    return items
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL004"

    def test_detects_dict_default(self, checker, temp_py_file):
        """Should detect dict as default argument."""
        code = '''
def update_config(key, value, config={}):
    config[key] = value
    return config
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_allows_none_default(self, checker, temp_py_file):
        """Should not flag None default with conditional initialization."""
        code = '''
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_allows_immutable_defaults(self, checker, temp_py_file):
        """Should not flag immutable defaults."""
        code = '''
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

def calculate(x, y, factor=1.5):
    return (x + y) * factor
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


class TestBroadExceptionChecker:
    """Tests for QUAL007: broad-exception."""

    @pytest.fixture
    def checker(self):
        return BroadExceptionChecker()

    def test_detects_bare_except(self, checker, temp_py_file):
        """Should detect bare except clause."""
        code = '''
try:
    risky_operation()
except:
    handle_error()
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "QUAL007" for f in findings)

    def test_detects_exception_catch(self, checker, temp_py_file):
        """Should detect catching base Exception."""
        code = '''
try:
    risky_operation()
except Exception:
    handle_error()
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1

    def test_allows_specific_exceptions(self, checker, temp_py_file):
        """Should not flag specific exception types."""
        code = '''
try:
    risky_operation()
except (ValueError, KeyError) as e:
    handle_error(e)
'''
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


class TestFileLengthChecker:
    """Tests for QUAL009: file-too-long."""

    @pytest.fixture
    def checker(self):
        return FileLengthChecker()

    def test_allows_short_file(self, checker, temp_py_file):
        """Should not flag short files."""
        code = "# Short file\n" * 100
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_warns_medium_file(self, checker, temp_py_file):
        """Should warn about medium-length files."""
        code = "# Line\n" * 600
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL009"
        assert findings[0].severity.value == "info"

    def test_errors_long_file(self, checker, temp_py_file):
        """Should error on very long files."""
        code = "# Line\n" * 1100
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL009"
        assert findings[0].severity.value == "warning"


# ============================================================================
# TypeScript Tests
# ============================================================================

class TestExceptionSwallowingCheckerTS:
    """TypeScript tests for QUAL001: exception-swallowing."""

    @pytest.fixture
    def checker(self):
        return ExceptionSwallowingChecker()

    def test_detects_empty_catch_ts(self, checker, temp_ts_file):
        """Should detect empty catch block in TypeScript (single line)."""
        code = '''
try { riskyOperation(); } catch (e) {}
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1
        assert any(f.rule_id == "QUAL001" for f in findings)

    def test_detects_promise_catch_noop_ts(self, checker, temp_ts_file):
        """Should detect empty promise .catch() in TypeScript."""
        code = '''
fetchData().catch((e) => {})
anotherCall().catch(() => undefined)
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) >= 1

    def test_allows_logged_exception_ts(self, checker, temp_ts_file):
        """Should not flag catch with logging in TypeScript."""
        code = '''
fetchData().catch((e) => { console.error("Failed:", e); })
'''
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0


class TestFileLengthCheckerTS:
    """TypeScript tests for QUAL009: file-too-long."""

    @pytest.fixture
    def checker(self):
        return FileLengthChecker()

    def test_allows_short_ts_file(self, checker, temp_ts_file):
        """Should not flag short TypeScript files."""
        code = "// Short file\n" * 100
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_warns_long_ts_file(self, checker, temp_ts_file):
        """Should warn about long TypeScript files."""
        code = "// Line\n" * 600
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL009"


class TestMixedBooleanOperatorsChecker:
    """Tests for QUAL013: mixed-boolean-operators."""

    @pytest.fixture
    def checker(self):
        return MixedBooleanOperatorsChecker()

    # ---- Python positive cases (should flag) ----

    def test_flags_python_if_mixed(self, checker, temp_py_file):
        """Should flag: if a or b and c."""
        code = "if a or b and c:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL013"

    def test_flags_python_elif_mixed(self, checker, temp_py_file):
        """Should flag: elif a or b and c."""
        code = "if False:\n    pass\nelif a or b and c:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL013"

    def test_flags_python_while_mixed(self, checker, temp_py_file):
        """Should flag: while a or b and c."""
        code = "while a or b and c:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL013"

    def test_flags_python_multiline(self, checker, temp_py_file):
        """Should flag multi-line Python condition with mixed operators."""
        code = "if (\n    a or\n    b and c\n):\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL013"

    def test_flags_python_multiple_mixed(self, checker, temp_py_file):
        """Should flag: if a or b and c or d."""
        code = "if a or b and c or d:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    def test_flags_python_unary_not(self, checker, temp_py_file):
        """Should flag: if not a or b and c (unary shouldn't confuse it)."""
        code = "if not a or b and c:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1

    # ---- JS/TS positive cases (should flag) ----

    def test_flags_js_mixed(self, checker, temp_js_file):
        """Should flag: if (a || b && c)."""
        code = "if (a || b && c) {\n  doSomething();\n}\n"
        path = temp_js_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL013"

    def test_flags_ts_mixed(self, checker, temp_ts_file):
        """Should flag: if (a || b && c) in TypeScript."""
        code = "if (a || b && c) {\n  doSomething();\n}\n"
        path = temp_ts_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL013"

    # ---- Go positive cases (should flag) ----

    def test_flags_go_mixed(self, checker, temp_go_file):
        """Should flag: if a || b && c { in Go."""
        code = "if a || b && c {\n  doSomething()\n}\n"
        path = temp_go_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 1
        assert findings[0].rule_id == "QUAL013"

    # ---- Negative cases (should NOT flag) ----

    def test_no_flag_only_and(self, checker, temp_py_file):
        """Should not flag: if a and b and c (only one operator type)."""
        code = "if a and b and c:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_only_or(self, checker, temp_py_file):
        """Should not flag: if a or b or c (only one operator type)."""
        code = "if a or b or c:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_parenthesized_and(self, checker, temp_py_file):
        """Should not flag: if a or (b and c) (properly parenthesized)."""
        code = "if a or (b and c):\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_parenthesized_or(self, checker, temp_py_file):
        """Should not flag: if (a or b) and c (properly parenthesized)."""
        code = "if (a or b) and c:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_js_parenthesized(self, checker, temp_js_file):
        """Should not flag: if ((a || b) && c) in JS."""
        code = "if ((a || b) && c) {\n  doSomething();\n}\n"
        path = temp_js_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_simple_and(self, checker, temp_py_file):
        """Should not flag: if a and b (simple, one operator type)."""
        code = "if a and b:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_string_contains_operator(self, checker, temp_py_file):
        """Should not false-positive on 'and' inside a string literal."""
        code = 'if "and" in text or flag:\n    pass\n'
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0

    def test_no_flag_comment_line(self, checker, temp_py_file):
        """Should not flag operators in comments."""
        code = "# check a or b and c\nif a and b:\n    pass\n"
        path = temp_py_file(code)
        findings = checker.check_file(path, code)
        assert len(findings) == 0
