"""Tests for MCP Secrets Detector — Advanced secret detection engine."""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from mcpaudit.secrets_detector import (
    DetectedSecret,
    SecretPatternDef,
    SecretsReport,
    _is_false_positive,
    _mask_value,
    _matches_allowlist,
    calculate_entropy,
    detect_secrets,
    detect_secrets_directory,
    detect_secrets_file,
    format_secrets_json,
    format_secrets_text,
    get_all_patterns,
    load_allowlist,
)


# ---------------------------------------------------------------------------
# calculate_entropy
# ---------------------------------------------------------------------------


class TestCalculateEntropy:
    def test_empty_string(self):
        assert calculate_entropy("") == 0.0

    def test_single_char(self):
        assert calculate_entropy("a") == 0.0

    def test_all_same_chars(self):
        assert calculate_entropy("aaaa") == 0.0

    def test_two_distinct_50_50(self):
        result = calculate_entropy("ab")
        assert abs(result - 1.0) < 0.01

    def test_high_entropy_random(self):
        result = calculate_entropy("aK3bZ9xQ7mN2wP5jR8cT4vL6yH1uF0")
        assert result > 3.5

    def test_low_entropy_repeated(self):
        result = calculate_entropy("aaaaabbbbb")
        assert result < 1.5

    def test_hex_string(self):
        result = calculate_entropy("0123456789abcdef0123456789abcdef")
        assert result > 3.0

    def test_base64_string(self):
        result = calculate_entropy("SGVsbG8gV29ybGQhIFRoaXMgaXMgYSB0ZXN0")
        assert result > 3.5


# ---------------------------------------------------------------------------
# _mask_value
# ---------------------------------------------------------------------------


class TestMaskValue:
    def test_long_value(self):
        assert _mask_value("AKIAIOSFODNN7EXAMPLE") == "AKIA***"

    def test_short_value(self):
        result = _mask_value("abc")
        assert result.endswith("***")

    def test_empty_value(self):
        assert _mask_value("") == "***"

    def test_four_char_value(self):
        result = _mask_value("abcd")
        assert result.startswith("a")
        assert "***" in result


# ---------------------------------------------------------------------------
# _is_false_positive
# ---------------------------------------------------------------------------


class TestIsFalsePositive:
    def test_short_value(self):
        assert _is_false_positive("abc") is True

    def test_placeholder_changeme(self):
        assert _is_false_positive("changeme") is True

    def test_placeholder_example(self):
        assert _is_false_positive("example_key_here") is True

    def test_placeholder_dummy(self):
        assert _is_false_positive("dummy_value_long") is True

    def test_all_same_chars(self):
        assert _is_false_positive("aaaaaaaaaa") is True

    def test_real_looking_secret(self):
        assert _is_false_positive("aK3bZ9xQ7mN2wP5jR8cT4vL6yH1uF0") is False

    def test_short_digits(self):
        assert _is_false_positive("12345") is True

    def test_long_digits(self):
        assert _is_false_positive("12345678") is False

    def test_placeholder_your_api_key(self):
        assert _is_false_positive("your_api_key") is True

    def test_none_value(self):
        assert _is_false_positive("none_here_long_val") is True


# ---------------------------------------------------------------------------
# DetectedSecret
# ---------------------------------------------------------------------------


class TestDetectedSecret:
    def test_to_dict(self):
        s = DetectedSecret(
            secret_type="aws_access_key",
            pattern_name="AWS Access Key ID",
            value_masked="AKIA***",
            file="test.py",
            line=5,
            entropy=3.45,
            confidence="HIGH",
            severity="CRITICAL",
            description="AWS Access Key ID detected",
            category="cloud",
        )
        d = s.to_dict()
        assert d["secret_type"] == "aws_access_key"
        assert d["entropy"] == 3.45
        assert d["category"] == "cloud"

    def test_default_values(self):
        s = DetectedSecret(
            secret_type="test",
            pattern_name="test",
            value_masked="t***",
            file="",
            line=0,
            entropy=0.0,
            confidence="LOW",
        )
        assert s.severity == "HIGH"
        assert s.description == ""
        assert s.category == ""


# ---------------------------------------------------------------------------
# SecretsReport
# ---------------------------------------------------------------------------


class TestSecretsReport:
    def test_empty_report(self):
        r = SecretsReport()
        assert r.exit_code == 0
        d = r.to_dict()
        assert d["total_secrets"] == 0

    def test_exit_code_with_secrets(self):
        r = SecretsReport(secrets=[
            DetectedSecret(
                secret_type="test", pattern_name="test", value_masked="t***",
                file="a.py", line=1, entropy=0.0, confidence="HIGH", severity="HIGH",
            ),
        ])
        assert r.exit_code == 1

    def test_exit_code_critical(self):
        r = SecretsReport(secrets=[
            DetectedSecret(
                secret_type="test", pattern_name="test", value_masked="t***",
                file="a.py", line=1, entropy=0.0, confidence="HIGH", severity="CRITICAL",
            ),
        ])
        assert r.exit_code == 2

    def test_by_category(self):
        r = SecretsReport(secrets=[
            DetectedSecret(
                secret_type="t1", pattern_name="p1", value_masked="v***",
                file="a.py", line=1, entropy=0.0, confidence="HIGH",
                category="cloud",
            ),
            DetectedSecret(
                secret_type="t2", pattern_name="p2", value_masked="v***",
                file="a.py", line=2, entropy=0.0, confidence="HIGH",
                category="cloud",
            ),
            DetectedSecret(
                secret_type="t3", pattern_name="p3", value_masked="v***",
                file="a.py", line=3, entropy=0.0, confidence="HIGH",
                category="vcs",
            ),
        ])
        d = r.to_dict()
        assert d["by_category"]["cloud"] == 2
        assert d["by_category"]["vcs"] == 1

    def test_by_severity(self):
        r = SecretsReport(secrets=[
            DetectedSecret(
                secret_type="t1", pattern_name="p1", value_masked="v***",
                file="a.py", line=1, entropy=0.0, confidence="HIGH", severity="CRITICAL",
            ),
            DetectedSecret(
                secret_type="t2", pattern_name="p2", value_masked="v***",
                file="a.py", line=2, entropy=0.0, confidence="HIGH", severity="HIGH",
            ),
        ])
        d = r.to_dict()
        assert d["by_severity"]["CRITICAL"] == 1
        assert d["by_severity"]["HIGH"] == 1


# ---------------------------------------------------------------------------
# get_all_patterns
# ---------------------------------------------------------------------------


class TestGetAllPatterns:
    def test_returns_patterns(self):
        patterns = get_all_patterns()
        assert len(patterns) >= 30

    def test_pattern_types(self):
        patterns = get_all_patterns()
        for p in patterns:
            assert isinstance(p, SecretPatternDef)
            assert p.name
            assert p.regex
            assert p.secret_type
            assert p.severity
            assert p.category

    def test_categories_present(self):
        patterns = get_all_patterns()
        categories = {p.category for p in patterns}
        assert "cloud" in categories
        assert "vcs" in categories
        assert "crypto" in categories
        assert "database" in categories


# ---------------------------------------------------------------------------
# load_allowlist
# ---------------------------------------------------------------------------


class TestLoadAllowlist:
    def test_nonexistent_file(self):
        result = load_allowlist("/nonexistent/file.txt")
        assert result == []

    def test_valid_allowlist(self, tmp_path):
        f = tmp_path / "allow.txt"
        f.write_text("# comment\ntest_pattern\n\nfoo.*bar\n")
        result = load_allowlist(str(f))
        assert len(result) == 2

    def test_invalid_regex_skipped(self, tmp_path):
        f = tmp_path / "allow.txt"
        f.write_text("[invalid\nvalid_pattern\n")
        result = load_allowlist(str(f))
        assert len(result) == 1


# ---------------------------------------------------------------------------
# _matches_allowlist
# ---------------------------------------------------------------------------


class TestMatchesAllowlist:
    def test_matches(self):
        import re
        patterns = [re.compile(r"test_.*")]
        assert _matches_allowlist("test_value", patterns) is True

    def test_no_match(self):
        import re
        patterns = [re.compile(r"test_.*")]
        assert _matches_allowlist("production_key", patterns) is False

    def test_empty_allowlist(self):
        assert _matches_allowlist("anything", []) is False


# ---------------------------------------------------------------------------
# detect_secrets (main detection)
# ---------------------------------------------------------------------------


class TestDetectSecrets:
    def test_aws_access_key(self):
        source = 'aws_key = "AKIAIOSFODNN7EXAMPLE1"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "aws_access_key" for f in findings)

    def test_github_pat(self):
        source = 'token = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef1234567"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "github_pat" for f in findings)

    def test_rsa_private_key(self):
        source = "-----BEGIN RSA PRIVATE KEY-----"
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "rsa_private_key" for f in findings)

    def test_ec_private_key(self):
        source = "-----BEGIN EC PRIVATE KEY-----"
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "ec_private_key" for f in findings)

    def test_generic_private_key(self):
        source = "-----BEGIN PRIVATE KEY-----"
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "private_key" for f in findings)

    def test_database_url(self):
        source = 'DATABASE_URL = "postgres://user:pass123456@localhost:5432/db"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "database_url" for f in findings)

    def test_slack_bot_token(self):
        source = 'SLACK_TOKEN = "xoxb-1234567890-abcdefghij"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "slack_bot_token" for f in findings)

    def test_stripe_key(self):
        source = 'stripe_key = "sk_live_ABCDEFGHIJKLMNOPQRSTnopqrstu"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "stripe_secret_key" for f in findings)

    def test_gcp_service_account(self):
        source = '{"type": "service_account", "project_id": "my-project"}'
        findings = detect_secrets(source, "test.json")
        assert any(f.secret_type == "gcp_service_account" for f in findings)

    def test_jwt_token(self):
        source = 'token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "jwt_token" for f in findings)

    def test_sendgrid_key(self):
        source = 'SG.abcdefghijklmnopqrstuvwx.abcdefghijklmnopqrstuvwx'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "sendgrid_api_key" for f in findings)

    def test_password_assignment(self):
        source = 'password = "SuperSecretPassword123!"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "hardcoded_password" for f in findings)

    def test_api_key_assignment(self):
        source = 'api_key = "aK3bZ9xQ7mN2wP5jR8"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "generic_api_key" for f in findings)

    def test_high_entropy_detection(self):
        source = 'token = "aK3bZ9xQ7mN2wP5jR8cT4vL6yH1uF0gE2dI5"'
        findings = detect_secrets(source, "test.py")
        assert len(findings) > 0

    def test_placeholder_ignored(self):
        source = 'api_key = "your_api_key_here"'
        findings = detect_secrets(source, "test.py")
        assert len(findings) == 0

    def test_changeme_ignored(self):
        source = 'password = "changeme"'
        findings = detect_secrets(source, "test.py")
        assert len(findings) == 0

    def test_allowlist_filter(self):
        import re
        source = 'api_key = "aK3bZ9xQ7mN2wP5jR8"'
        allowlist = [re.compile(r"aK3b.*")]
        findings = detect_secrets(source, "test.py", allowlist=allowlist)
        api_keys = [f for f in findings if f.secret_type == "generic_api_key"]
        assert len(api_keys) == 0

    def test_custom_entropy_threshold(self):
        source = 'token = "ABCDEFGHIJKLmnopqrstuvwx"'
        low_thresh = detect_secrets(source, "test.py", entropy_threshold=2.0)
        high_thresh = detect_secrets(source, "test.py", entropy_threshold=6.0)
        assert len(low_thresh) >= len(high_thresh)

    def test_no_false_positive_on_clean_code(self):
        source = """
import os
import sys

def main():
    print("Hello, world!")
    return 0
"""
        findings = detect_secrets(source, "test.py")
        assert len(findings) == 0

    def test_multiple_secrets_same_file(self):
        source = """
aws_key = "AKIAIOSFODNN7EXAMPLE1"
password = "SuperSecretPassword123!"
"""
        findings = detect_secrets(source, "test.py")
        types = {f.secret_type for f in findings}
        assert "aws_access_key" in types
        assert "hardcoded_password" in types

    def test_dedup_same_line(self):
        source = 'AKIAIOSFODNN7EXAMPLE1 AKIAIOSFODNN7EXAMPLE1'
        findings = detect_secrets(source, "test.py")
        aws_findings = [f for f in findings if f.secret_type == "aws_access_key"]
        assert len(aws_findings) == 1

    def test_pgp_private_key(self):
        source = "-----BEGIN PGP PRIVATE KEY BLOCK-----"
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "pgp_private_key" for f in findings)

    def test_ssh_private_key(self):
        source = "-----BEGIN OPENSSH PRIVATE KEY-----"
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "ssh_private_key" for f in findings)

    def test_slack_webhook(self):
        source = "url = https://hooks.slack.com/services/T12345678/B12345678/abcdefgh12345678"
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "slack_webhook" for f in findings)

    def test_discord_webhook(self):
        source = "url = https://discord.com/api/webhooks/123456789/abcdefgh_IJKLMNOP"
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "discord_webhook" for f in findings)

    def test_gitlab_pat(self):
        source = 'token = "glpat-ABCDEFGHIJKLMNOPQRSTuvwx"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "gitlab_pat" for f in findings)

    def test_heroku_api_key(self):
        source = 'HEROKU_API_KEY = "12345678-1234-1234-1234-123456789abc"'
        findings = detect_secrets(source, "test.py")
        assert any(f.secret_type == "heroku_api_key" for f in findings)


# ---------------------------------------------------------------------------
# detect_secrets_file
# ---------------------------------------------------------------------------


class TestDetectSecretsFile:
    def test_scan_file(self, tmp_path):
        f = tmp_path / "test.py"
        f.write_text('api_key = "aK3bZ9xQ7mN2wP5jR8"\n')
        findings = detect_secrets_file(str(f))
        assert len(findings) > 0

    def test_nonexistent_file(self):
        findings = detect_secrets_file("/nonexistent/file.py")
        assert findings == []

    def test_with_allowlist(self, tmp_path):
        import re
        f = tmp_path / "test.py"
        f.write_text('api_key = "aK3bZ9xQ7mN2wP5jR8"\n')
        allowlist = [re.compile(r"aK3b.*")]
        findings = detect_secrets_file(str(f), allowlist=allowlist)
        api_keys = [fnd for fnd in findings if fnd.secret_type == "generic_api_key"]
        assert len(api_keys) == 0


# ---------------------------------------------------------------------------
# detect_secrets_directory
# ---------------------------------------------------------------------------


class TestDetectSecretsDirectory:
    def test_scan_directory(self, tmp_path):
        (tmp_path / "a.py").write_text('api_key = "aK3bZ9xQ7mN2wP5jR8"\n')
        (tmp_path / "b.py").write_text("# clean file\n")
        report = detect_secrets_directory(str(tmp_path))
        assert report.files_scanned == 2
        assert len(report.secrets) > 0

    def test_single_file_fallback(self, tmp_path):
        f = tmp_path / "test.py"
        f.write_text('password = "SuperSecretPassword123!"\n')
        report = detect_secrets_directory(str(f))
        assert report.files_scanned == 1
        assert len(report.secrets) > 0

    def test_empty_directory(self, tmp_path):
        report = detect_secrets_directory(str(tmp_path))
        assert report.files_scanned == 0
        assert len(report.secrets) == 0

    def test_report_metadata(self, tmp_path):
        (tmp_path / "a.py").write_text("# clean\n")
        report = detect_secrets_directory(str(tmp_path))
        assert report.patterns_used >= 30
        assert report.entropy_threshold == 4.5

    def test_custom_entropy(self, tmp_path):
        (tmp_path / "a.py").write_text("# clean\n")
        report = detect_secrets_directory(str(tmp_path), entropy_threshold=5.0)
        assert report.entropy_threshold == 5.0


# ---------------------------------------------------------------------------
# Output formatters
# ---------------------------------------------------------------------------


class TestFormatSecretsText:
    def test_empty_report(self):
        report = SecretsReport()
        text = format_secrets_text(report)
        assert "No secrets detected" in text

    def test_with_findings(self):
        report = SecretsReport(
            secrets=[
                DetectedSecret(
                    secret_type="aws_access_key",
                    pattern_name="AWS Access Key ID",
                    value_masked="AKIA***",
                    file="test.py",
                    line=5,
                    entropy=3.5,
                    confidence="HIGH",
                    severity="CRITICAL",
                    description="AWS Access Key ID detected",
                    category="cloud",
                ),
            ],
            files_scanned=1,
            patterns_used=36,
        )
        text = format_secrets_text(report)
        assert "Secrets Detector" in text
        assert "CRITICAL" in text
        assert "AKIA***" in text
        assert "cloud" in text

    def test_severity_ordering(self):
        report = SecretsReport(
            secrets=[
                DetectedSecret(
                    secret_type="t1", pattern_name="p1", value_masked="v***",
                    file="a.py", line=1, entropy=0.0, confidence="HIGH",
                    severity="MEDIUM",
                ),
                DetectedSecret(
                    secret_type="t2", pattern_name="p2", value_masked="v***",
                    file="a.py", line=2, entropy=0.0, confidence="HIGH",
                    severity="CRITICAL",
                ),
            ],
        )
        text = format_secrets_text(report)
        crit_pos = text.index("CRITICAL")
        med_pos = text.index("MEDIUM")
        assert crit_pos < med_pos


class TestFormatSecretsJson:
    def test_json_structure(self):
        report = SecretsReport(
            secrets=[
                DetectedSecret(
                    secret_type="test", pattern_name="Test", value_masked="t***",
                    file="a.py", line=1, entropy=3.0, confidence="HIGH",
                ),
            ],
            files_scanned=5,
            patterns_used=36,
        )
        text = format_secrets_json(report)
        data = json.loads(text)
        assert "secrets_detector" in data
        assert data["secrets_detector"]["total_secrets"] == 1
        assert data["secrets_detector"]["files_scanned"] == 5

    def test_empty_json(self):
        report = SecretsReport()
        text = format_secrets_json(report)
        data = json.loads(text)
        assert data["secrets_detector"]["total_secrets"] == 0
