"""Tests for Credential Scanner."""

from __future__ import annotations

import json
import os
from pathlib import Path

import pytest

from mcpaudit.credential_scanner import (
    BUILTIN_PATTERNS,
    AllowlistEntry,
    CredSeverity,
    CredentialFinding,
    CredentialPattern,
    CredentialScanner,
    ScanResult,
    _shannon_entropy,
    format_json,
    format_sarif,
    format_text,
    is_high_entropy,
    mask_value,
)


# ---------- CredSeverity ----------

class TestCredSeverity:
    def test_critical(self):
        assert CredSeverity.CRITICAL == "CRITICAL"

    def test_high(self):
        assert CredSeverity.HIGH == "HIGH"

    def test_medium(self):
        assert CredSeverity.MEDIUM == "MEDIUM"

    def test_low(self):
        assert CredSeverity.LOW == "LOW"

    def test_info(self):
        assert CredSeverity.INFO == "INFO"


# ---------- CredentialPattern ----------

class TestCredentialPattern:
    def test_creation(self):
        p = CredentialPattern(name="test", regex=r"test_\w+")
        assert p.name == "test"
        assert p.severity == CredSeverity.HIGH

    def test_to_dict(self):
        p = CredentialPattern(
            name="test", regex=r"\w+",
            severity=CredSeverity.CRITICAL,
            description="Test pattern",
        )
        d = p.to_dict()
        assert d["name"] == "test"
        assert d["severity"] == "CRITICAL"
        assert d["description"] == "Test pattern"


# ---------- CredentialFinding ----------

class TestCredentialFinding:
    def test_creation(self):
        f = CredentialFinding(pattern_name="aws_key")
        assert f.pattern_name == "aws_key"
        assert f.file == ""
        assert f.line == 0

    def test_to_dict(self):
        f = CredentialFinding(
            pattern_name="aws_key",
            file="config.py",
            line=10,
            masked_value="AKIA****",
            severity=CredSeverity.CRITICAL,
            description="AWS Key",
            column=5,
            match_length=20,
        )
        d = f.to_dict()
        assert d["pattern_name"] == "aws_key"
        assert d["file"] == "config.py"
        assert d["line"] == 10
        assert d["masked_value"] == "AKIA****"
        assert d["match_length"] == 20


# ---------- ScanResult ----------

class TestScanResult:
    def test_empty(self):
        r = ScanResult()
        assert r.total_findings == 0
        assert r.exit_code == 0
        assert r.files_scanned == 0

    def test_severity_counts(self):
        r = ScanResult(findings=[
            CredentialFinding("a", severity=CredSeverity.CRITICAL),
            CredentialFinding("b", severity=CredSeverity.CRITICAL),
            CredentialFinding("c", severity=CredSeverity.HIGH),
        ])
        sc = r.severity_counts
        assert sc["CRITICAL"] == 2
        assert sc["HIGH"] == 1
        assert sc["MEDIUM"] == 0

    def test_exit_code_critical(self):
        r = ScanResult(findings=[
            CredentialFinding("a", severity=CredSeverity.CRITICAL),
        ])
        assert r.exit_code == 2

    def test_exit_code_high(self):
        r = ScanResult(findings=[
            CredentialFinding("a", severity=CredSeverity.HIGH),
        ])
        assert r.exit_code == 1

    def test_exit_code_clean(self):
        r = ScanResult(findings=[
            CredentialFinding("a", severity=CredSeverity.LOW),
        ])
        assert r.exit_code == 0

    def test_to_dict(self):
        r = ScanResult(files_scanned=5, lines_scanned=100)
        d = r.to_dict()
        assert d["files_scanned"] == 5
        assert d["lines_scanned"] == 100
        assert d["total_findings"] == 0


# ---------- Masking ----------

class TestMaskValue:
    def test_normal(self):
        assert mask_value("sk_live_abcdefghij1234567890") == "sk_l********************"

    def test_short_value(self):
        result = mask_value("abc")
        assert result == "***"

    def test_empty(self):
        assert mask_value("") == "****"

    def test_custom_visible(self):
        assert mask_value("AKIAIOSFODNN7EXAMPLE", visible_chars=6) == "AKIAIO**************"

    def test_exact_visible_chars(self):
        result = mask_value("abcd", visible_chars=4)
        assert result == "****"


# ---------- Entropy ----------

class TestShannonEntropy:
    def test_empty_string(self):
        assert _shannon_entropy("") == 0.0

    def test_single_char(self):
        assert _shannon_entropy("aaaa") == 0.0

    def test_two_chars_equal(self):
        e = _shannon_entropy("aabb")
        assert abs(e - 1.0) < 0.01

    def test_high_entropy(self):
        e = _shannon_entropy("aB3$xY9!mK7@")
        assert e > 3.0

    def test_low_entropy(self):
        e = _shannon_entropy("aaaaaaaaaa")
        assert e == 0.0


class TestIsHighEntropy:
    def test_random_string(self):
        # A random-looking string with high entropy
        assert is_high_entropy("aB3xY9mK7qW2eR5tU8iO0pL4jH6gF1d") is True

    def test_low_entropy_string(self):
        assert is_high_entropy("aaaaaaaaaaaaaaaaaaaaaa") is False

    def test_short_string(self):
        assert is_high_entropy("abc") is False

    def test_custom_threshold(self):
        s = "abcdefghijklmnopqrst"
        assert is_high_entropy(s, threshold=2.0) is True


# ---------- AllowlistEntry ----------

class TestAllowlistEntry:
    def test_match_pattern_name(self):
        entry = AllowlistEntry(pattern_name="generic_password")
        finding = CredentialFinding(pattern_name="generic_password")
        assert entry.matches(finding) is True

    def test_no_match_pattern_name(self):
        entry = AllowlistEntry(pattern_name="generic_password")
        finding = CredentialFinding(pattern_name="aws_key")
        assert entry.matches(finding) is False

    def test_match_file_pattern(self):
        entry = AllowlistEntry(file_pattern="test_")
        finding = CredentialFinding(pattern_name="x", file="test_config.py")
        assert entry.matches(finding) is True

    def test_match_value_pattern(self):
        entry = AllowlistEntry(value_pattern="exam")
        finding = CredentialFinding(pattern_name="x", masked_value="exam****")
        assert entry.matches(finding) is True

    def test_empty_entry_matches_all(self):
        entry = AllowlistEntry()
        finding = CredentialFinding(pattern_name="anything")
        assert entry.matches(finding) is True

    def test_combined_criteria(self):
        entry = AllowlistEntry(pattern_name="generic_password", file_pattern="test")
        finding = CredentialFinding(pattern_name="generic_password", file="test_file.py")
        assert entry.matches(finding) is True

    def test_combined_criteria_partial_fail(self):
        entry = AllowlistEntry(pattern_name="generic_password", file_pattern="production")
        finding = CredentialFinding(pattern_name="generic_password", file="test_file.py")
        assert entry.matches(finding) is False


# ---------- Built-in Patterns ----------

class TestBuiltinPatterns:
    def test_at_least_25_patterns(self):
        assert len(BUILTIN_PATTERNS) >= 25

    def test_all_have_names(self):
        for p in BUILTIN_PATTERNS:
            assert p.name, f"Pattern missing name: {p}"

    def test_all_have_regex(self):
        for p in BUILTIN_PATTERNS:
            assert p.regex, f"Pattern missing regex: {p.name}"

    def test_all_have_severity(self):
        valid = {"CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"}
        for p in BUILTIN_PATTERNS:
            assert p.severity in valid, f"Invalid severity in {p.name}: {p.severity}"

    def test_all_regex_compile(self):
        import re
        for p in BUILTIN_PATTERNS:
            try:
                re.compile(p.regex, re.IGNORECASE)
            except re.error as e:
                pytest.fail(f"Invalid regex in {p.name}: {e}")


# ---------- CredentialScanner ----------

class TestCredentialScanner:
    def setup_method(self):
        self.scanner = CredentialScanner()

    def test_detect_aws_key(self):
        content = 'aws_key = "AKIAIOSFODNN7EXAMPLE"'
        findings = self.scanner.scan_string(content)
        pattern_names = [f.pattern_name for f in findings]
        assert "aws_access_key" in pattern_names

    def test_detect_github_token(self):
        content = 'token = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij"'
        findings = self.scanner.scan_string(content)
        pattern_names = [f.pattern_name for f in findings]
        assert "github_token" in pattern_names

    def test_detect_private_key(self):
        content = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBA...\n-----END RSA PRIVATE KEY-----"
        findings = self.scanner.scan_string(content)
        pattern_names = [f.pattern_name for f in findings]
        assert "private_key_header" in pattern_names

    def test_detect_generic_password(self):
        content = 'password = "my_super_secret_password123"'
        findings = self.scanner.scan_string(content)
        pattern_names = [f.pattern_name for f in findings]
        assert "generic_password" in pattern_names

    def test_detect_database_url(self):
        content = 'db_url = "postgres://admin:secret@localhost:5432/mydb"'
        findings = self.scanner.scan_string(content)
        pattern_names = [f.pattern_name for f in findings]
        assert "database_url" in pattern_names

    def test_no_false_positive_on_clean_code(self):
        content = """
def greet(name):
    print(f"Hello, {name}!")
    return True
"""
        findings = self.scanner.scan_string(content)
        assert len(findings) == 0

    def test_masked_values_never_expose_secrets(self):
        content = 'password = "my_super_secret_password123"'
        findings = self.scanner.scan_string(content)
        for f in findings:
            assert "super_secret_password" not in f.masked_value

    def test_line_numbers_correct(self):
        content = 'line1\nline2\npassword = "my_super_secret_password123"\nline4'
        findings = self.scanner.scan_string(content)
        pwd_findings = [f for f in findings if f.pattern_name == "generic_password"]
        assert len(pwd_findings) >= 1
        assert pwd_findings[0].line == 3

    def test_filename_assigned(self):
        content = 'password = "my_super_secret_password123"'
        findings = self.scanner.scan_string(content, filename="config.py")
        assert all(f.file == "config.py" for f in findings)

    def test_custom_pattern(self):
        scanner = CredentialScanner(patterns=[
            CredentialPattern("custom", r"MY_SECRET_[A-Z]{10}", CredSeverity.HIGH),
        ])
        content = 'key = "MY_SECRET_ABCDEFGHIJ"'
        findings = scanner.scan_string(content)
        assert len(findings) >= 1
        assert findings[0].pattern_name == "custom"

    def test_allowlist_suppresses(self):
        scanner = CredentialScanner(
            allowlist=[AllowlistEntry(pattern_name="generic_password", file_pattern="test")],
        )
        content = 'password = "test_password_value_here"'
        findings = scanner.scan_string(content, filename="test_config.py")
        pwd_findings = [f for f in findings if f.pattern_name == "generic_password"]
        assert len(pwd_findings) == 0

    def test_add_pattern(self):
        self.scanner.add_pattern(
            CredentialPattern("custom2", r"CUST_[0-9]{8}", CredSeverity.LOW),
        )
        content = 'id = "CUST_12345678"'
        findings = self.scanner.scan_string(content)
        assert any(f.pattern_name == "custom2" for f in findings)

    def test_add_allowlist_entry(self):
        self.scanner.add_allowlist_entry(
            AllowlistEntry(pattern_name="generic_password"),
        )
        content = 'password = "my_super_secret_password123"'
        findings = self.scanner.scan_string(content)
        assert not any(f.pattern_name == "generic_password" for f in findings)

    def test_scan_file(self, tmp_path):
        f = tmp_path / "config.py"
        f.write_text('password = "my_super_secret_password123"\n')
        findings = self.scanner.scan_file(f)
        assert len(findings) >= 1

    def test_scan_file_skip_binary(self, tmp_path):
        f = tmp_path / "image.png"
        f.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
        findings = self.scanner.scan_file(f)
        assert len(findings) == 0

    def test_scan_directory(self, tmp_path):
        (tmp_path / "app.py").write_text('password = "my_super_secret_password123"\n')
        (tmp_path / "clean.py").write_text("x = 1\n")
        result = self.scanner.scan_directory(tmp_path)
        assert result.files_scanned == 2
        assert result.total_findings >= 1

    def test_scan_directory_recursive(self, tmp_path):
        sub = tmp_path / "subdir"
        sub.mkdir()
        (sub / "config.py").write_text('secret = "my_very_secret_value_here"\n')
        result = self.scanner.scan_directory(tmp_path, recursive=True)
        assert result.total_findings >= 1

    def test_scan_directory_skip_gitdir(self, tmp_path):
        git_dir = tmp_path / ".git"
        git_dir.mkdir()
        (git_dir / "config").write_text('password = "my_super_secret_password123"\n')
        (tmp_path / "app.py").write_text("x = 1\n")
        result = self.scanner.scan_directory(tmp_path)
        pwd_findings = [f for f in result.findings if f.pattern_name == "generic_password"]
        assert len(pwd_findings) == 0

    def test_scan_directory_not_a_dir(self):
        result = self.scanner.scan_directory(Path("/nonexistent/path"))
        assert len(result.errors) > 0

    def test_entropy_detection(self):
        scanner = CredentialScanner(
            patterns=[],  # No regex patterns, only entropy
            entropy_check=True,
            entropy_threshold=4.0,
        )
        content = 'token = "aB3xY9mK7qW2eR5tU8iO0pL4jH6gF1dzQ"'
        findings = scanner.scan_string(content)
        assert any(f.pattern_name == "high_entropy" for f in findings)

    def test_entropy_disabled(self):
        scanner = CredentialScanner(
            patterns=[],
            entropy_check=False,
        )
        content = 'token = "aB3xY9mK7qW2eR5tU8iO0pL4jH6gF1dzQ"'
        findings = scanner.scan_string(content)
        assert len(findings) == 0

    def test_detect_stripe_key(self):
        content = 'key = "sk_live_ABCDEFGHIJKLMNOPQRSTx"'
        findings = self.scanner.scan_string(content)
        assert any(f.pattern_name == "stripe_secret" for f in findings)

    def test_detect_slack_token(self):
        content = 'token = "xoxb-123456789012-abcdef"'
        findings = self.scanner.scan_string(content)
        assert any(f.pattern_name == "slack_token" for f in findings)


# ---------- Formatters ----------

class TestFormatText:
    def test_basic_output(self):
        r = ScanResult(
            findings=[CredentialFinding(
                "aws_key", file="config.py", line=5,
                masked_value="AKIA****", severity=CredSeverity.CRITICAL,
                description="AWS Key",
            )],
            files_scanned=1,
            lines_scanned=10,
        )
        text = format_text(r)
        assert "Credential Scanner" in text
        assert "CRITICAL" in text
        assert "AKIA****" in text

    def test_empty_results(self):
        r = ScanResult()
        text = format_text(r)
        assert "Findings:      0" in text


class TestFormatJson:
    def test_valid_json(self):
        r = ScanResult(findings=[
            CredentialFinding("test", severity=CredSeverity.HIGH),
        ])
        data = json.loads(format_json(r))
        assert data["total_findings"] == 1

    def test_empty(self):
        data = json.loads(format_json(ScanResult()))
        assert data["total_findings"] == 0


class TestFormatSarif:
    def test_valid_sarif(self):
        r = ScanResult(findings=[
            CredentialFinding(
                "aws_key", file="config.py", line=5,
                severity=CredSeverity.CRITICAL,
                description="AWS Key",
            ),
        ])
        data = json.loads(format_sarif(r))
        assert data["version"] == "2.1.0"
        assert len(data["runs"]) == 1
        assert len(data["runs"][0]["results"]) == 1

    def test_sarif_rule_ids(self):
        r = ScanResult(findings=[
            CredentialFinding("aws_key", severity=CredSeverity.CRITICAL, description="AWS"),
            CredentialFinding("github_token", severity=CredSeverity.HIGH, description="GH"),
        ])
        data = json.loads(format_sarif(r))
        rules = data["runs"][0]["tool"]["driver"]["rules"]
        assert len(rules) == 2

    def test_empty_sarif(self):
        data = json.loads(format_sarif(ScanResult()))
        assert len(data["runs"][0]["results"]) == 0
