"""Tests for secret/PII scanner module."""
import time

import pytest

from greymatter_core.scanner import Detection, ScannerConfig, SecretScanner


@pytest.fixture
def scanner():
    return SecretScanner()


@pytest.fixture
def strict_scanner():
    return SecretScanner(ScannerConfig(mode="block"))


# =============================================================================
# AWS credential detection
# =============================================================================


def test_detect_aws_access_key(scanner):
    text = "Use key AKIAIOSFODNN7EXAMPLE for access"
    detections = scanner.scan(text)
    assert len(detections) >= 1
    aws = [d for d in detections if d.pattern_name == "aws_access_key"]
    assert len(aws) == 1
    assert aws[0].matched_text == "AKIAIOSFODNN7EXAMPLE"
    assert aws[0].confidence >= 0.95
    assert aws[0].severity == "critical"
    assert aws[0].category == "cloud_credential"


def test_detect_aws_secret_key(scanner):
    text = "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    detections = scanner.scan(text)
    secret = [d for d in detections if d.pattern_name == "aws_secret_key"]
    assert len(secret) == 1
    assert secret[0].confidence >= 0.9
    assert secret[0].severity == "critical"


# =============================================================================
# Token detection
# =============================================================================


def test_detect_github_token(scanner):
    text = "export GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12"
    detections = scanner.scan(text)
    gh = [d for d in detections if d.pattern_name == "github_token"]
    assert len(gh) == 1
    assert gh[0].matched_text.startswith("ghp_")
    assert gh[0].severity == "critical"


def test_detect_gitlab_token(scanner):
    text = "GITLAB_TOKEN=glpat-ABCDEFGHIJKLMNOPQRSTm"
    detections = scanner.scan(text)
    gl = [d for d in detections if d.pattern_name == "gitlab_token"]
    assert len(gl) == 1


def test_detect_slack_token(scanner):
    text = "token: xoxb-123456789012-123456789012-ABCdefGHIjklMNOpqrSTUvwx"
    detections = scanner.scan(text)
    slack = [d for d in detections if d.pattern_name == "slack_token"]
    assert len(slack) == 1
    assert slack[0].severity == "critical"


# =============================================================================
# Private key detection
# =============================================================================


def test_detect_private_key(scanner):
    text = """Here is my key:
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF
-----END RSA PRIVATE KEY-----
Done"""
    detections = scanner.scan(text)
    pk = [d for d in detections if d.pattern_name == "private_key"]
    assert len(pk) == 1
    assert pk[0].severity == "critical"
    assert pk[0].category == "cryptographic_key"


def test_detect_openssh_private_key(scanner):
    text = "-----BEGIN OPENSSH PRIVATE KEY-----\nbase64data\n-----END OPENSSH PRIVATE KEY-----"
    detections = scanner.scan(text)
    pk = [d for d in detections if d.pattern_name == "private_key"]
    assert len(pk) == 1


# =============================================================================
# Password detection
# =============================================================================


def test_detect_password_in_config(scanner):
    text = 'password=SuperSecret123!'
    detections = scanner.scan(text)
    pwd = [d for d in detections if d.pattern_name == "password_in_config"]
    assert len(pwd) == 1
    assert pwd[0].severity == "critical"


def test_detect_passwd_variant(scanner):
    text = "db_passwd=MyP@ssw0rd"
    detections = scanner.scan(text)
    pwd = [d for d in detections if d.pattern_name == "password_in_config"]
    assert len(pwd) == 1


# =============================================================================
# Connection string detection
# =============================================================================


def test_detect_connection_string(scanner):
    text = "DATABASE_URL=postgresql://admin:secret123@db.example.com:5432/mydb"
    detections = scanner.scan(text)
    conn = [d for d in detections if d.pattern_name == "connection_string"]
    assert len(conn) == 1
    assert conn[0].severity == "critical"
    assert conn[0].category == "connection_string"


def test_detect_mongodb_connection_string(scanner):
    text = "MONGO_URI=mongodb://root:password@mongo.internal:27017/app"
    detections = scanner.scan(text)
    conn = [d for d in detections if d.pattern_name == "connection_string"]
    assert len(conn) == 1


# =============================================================================
# Credit card detection with Luhn validation
# =============================================================================


def test_detect_credit_card_valid_luhn(scanner):
    # 4111 1111 1111 1111 is a valid Luhn test number (Visa)
    text = "Card: 4111111111111111"
    detections = scanner.scan(text)
    cc = [d for d in detections if d.pattern_name == "credit_card"]
    assert len(cc) == 1
    assert cc[0].severity == "high"
    assert cc[0].category == "financial"


def test_reject_credit_card_invalid_luhn(scanner):
    # 1234567890123456 does NOT pass Luhn
    text = "Number: 1234567890123456"
    detections = scanner.scan(text)
    cc = [d for d in detections if d.pattern_name == "credit_card"]
    assert len(cc) == 0


def test_detect_credit_card_with_spaces(scanner):
    text = "Card: 4111 1111 1111 1111"
    detections = scanner.scan(text)
    cc = [d for d in detections if d.pattern_name == "credit_card"]
    assert len(cc) == 1


def test_detect_credit_card_with_dashes(scanner):
    text = "Card: 4111-1111-1111-1111"
    detections = scanner.scan(text)
    cc = [d for d in detections if d.pattern_name == "credit_card"]
    assert len(cc) == 1


# =============================================================================
# SSN detection
# =============================================================================


def test_detect_ssn(scanner):
    text = "SSN: 123-45-6789"
    detections = scanner.scan(text)
    ssn = [d for d in detections if d.pattern_name == "ssn"]
    assert len(ssn) == 1
    assert ssn[0].severity == "critical"
    assert ssn[0].category == "pii"


def test_reject_invalid_ssn(scanner):
    # SSNs starting with 000, 666, or 900-999 are invalid
    text = "SSN: 000-12-3456"
    detections = scanner.scan(text)
    ssn = [d for d in detections if d.pattern_name == "ssn"]
    assert len(ssn) == 0


# =============================================================================
# Generic secret detection
# =============================================================================


def test_detect_generic_api_key(scanner):
    text = "api_key=sk-1234567890abcdef1234567890abcdef"
    detections = scanner.scan(text)
    # Should match either generic_secret or api_key pattern
    api = [d for d in detections if "api_key" in d.pattern_name or "generic" in d.pattern_name]
    assert len(api) >= 1


def test_detect_bearer_token(scanner):
    text = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N"
    detections = scanner.scan(text)
    bearer = [d for d in detections if d.pattern_name == "bearer_token"]
    assert len(bearer) == 1
    assert bearer[0].severity == "high"


# =============================================================================
# Email detection (configurable)
# =============================================================================


def test_detect_email_when_enabled(scanner):
    text = "Contact: alice@example.com for details"
    detections = scanner.scan(text)
    email = [d for d in detections if d.pattern_name == "email_address"]
    assert len(email) == 1
    assert email[0].severity == "low"
    assert email[0].category == "pii"


def test_email_disabled_by_config():
    config = ScannerConfig(categories=["cloud_credential", "cryptographic_key"])
    scanner = SecretScanner(config)
    text = "Contact: alice@example.com for details"
    detections = scanner.scan(text)
    email = [d for d in detections if d.pattern_name == "email_address"]
    assert len(email) == 0


# =============================================================================
# Redaction
# =============================================================================


def test_redact_replaces_with_type_placeholder(scanner):
    text = "Use key AKIAIOSFODNN7EXAMPLE for access"
    redacted = scanner.redact(text)
    assert "AKIAIOSFODNN7EXAMPLE" not in redacted
    assert "[AWS_ACCESS_KEY_REDACTED]" in redacted


def test_redact_multiple_secrets(scanner):
    text = "key=AKIAIOSFODNN7EXAMPLE password=Secret123"
    redacted = scanner.redact(text)
    assert "AKIAIOSFODNN7EXAMPLE" not in redacted
    assert "Secret123" not in redacted


def test_redact_preserves_surrounding_text(scanner):
    text = "Before AKIAIOSFODNN7EXAMPLE after"
    redacted = scanner.redact(text)
    assert redacted.startswith("Before ")
    assert redacted.endswith(" after")


def test_redact_with_explicit_detections(scanner):
    text = "password=Secret123"
    detections = scanner.scan(text)
    redacted = scanner.redact(text, detections)
    assert "Secret123" not in redacted


# =============================================================================
# No false positives
# =============================================================================


def test_no_false_positive_normal_code(scanner):
    text = """
def calculate_total(items):
    total = sum(item.price for item in items)
    return total * 1.08  # tax

class UserService:
    def __init__(self, db_connection):
        self.db = db_connection

    def get_user(self, user_id: str):
        return self.db.query("SELECT * FROM users WHERE id = ?", user_id)
"""
    detections = scanner.scan(text)
    # Should have zero or very few low-confidence detections
    high_confidence = [d for d in detections if d.confidence >= 0.8]
    assert len(high_confidence) == 0


def test_no_false_positive_normal_text(scanner):
    text = """
The deployment process involves three stages:
1. Build the Docker image
2. Push to the container registry
3. Deploy to the Kubernetes cluster

The application uses environment variables for configuration.
Make sure to set the DATABASE_URL before starting the server.
"""
    detections = scanner.scan(text)
    high_confidence = [d for d in detections if d.confidence >= 0.8]
    assert len(high_confidence) == 0


def test_no_false_positive_short_strings(scanner):
    """Short key-like words should not trigger false positives."""
    text = "The key insight is that patterns emerge over time."
    detections = scanner.scan(text)
    secrets = [d for d in detections if d.severity in ("critical", "high")]
    assert len(secrets) == 0


# =============================================================================
# Overlap resolution
# =============================================================================


def test_overlapping_patterns_prefer_longer_match(scanner):
    # A string that could match multiple patterns: prefer the longer/higher-confidence one
    text = "api_key=sk-1234567890abcdef1234567890abcdef"
    detections = scanner.scan(text)
    # There should be no overlapping detections at the same position
    for i, d1 in enumerate(detections):
        for d2 in detections[i + 1:]:
            overlaps = d1.start < d2.end and d1.end > d2.start
            assert not overlaps, f"Overlapping: {d1.pattern_name}[{d1.start}:{d1.end}] vs {d2.pattern_name}[{d2.start}:{d2.end}]"


# =============================================================================
# Configuration
# =============================================================================


def test_disabled_scanner_returns_no_detections():
    config = ScannerConfig(enabled=False)
    scanner = SecretScanner(config)
    text = "AKIAIOSFODNN7EXAMPLE password=secret ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12"
    assert scanner.scan(text) == []
    assert scanner.has_secrets(text) is False


def test_min_confidence_filters_low_confidence():
    config = ScannerConfig(min_confidence=0.95)
    scanner = SecretScanner(config)
    # Email has low confidence (0.5) so should be filtered
    text = "Contact: alice@example.com with key AKIAIOSFODNN7EXAMPLE"
    detections = scanner.scan(text)
    email = [d for d in detections if d.pattern_name == "email_address"]
    assert len(email) == 0
    # AWS key has high confidence and should still be detected
    aws = [d for d in detections if d.pattern_name == "aws_access_key"]
    assert len(aws) == 1


def test_category_filter():
    config = ScannerConfig(categories=["cloud_credential"])
    scanner = SecretScanner(config)
    text = "AKIAIOSFODNN7EXAMPLE and SSN: 123-45-6789"
    detections = scanner.scan(text)
    # Only cloud_credential category should be detected
    categories = {d.category for d in detections}
    assert "cloud_credential" in categories
    assert "pii" not in categories


def test_has_secrets_convenience(scanner):
    assert scanner.has_secrets("AKIAIOSFODNN7EXAMPLE") is True
    assert scanner.has_secrets("just normal text here") is False


# =============================================================================
# Performance
# =============================================================================


def test_performance_10kb_text(scanner):
    """Scanning 10KB of text should complete in under 50ms."""
    # Build a 10KB text block with some embedded secrets
    normal = "This is a normal line of code that does nothing special. " * 20 + "\n"
    text = normal * 50  # ~50KB of clean text
    text += "\nAKIAIOSFODNN7EXAMPLE\n"  # Add one secret
    text += normal * 5

    start = time.time()
    for _ in range(10):  # Run 10 times for more stable timing
        detections = scanner.scan(text)
    elapsed = (time.time() - start) / 10

    assert elapsed < 0.05, f"Scan took {elapsed:.4f}s, expected <0.05s"
    assert len(detections) >= 1


# =============================================================================
# Mode behavior
# =============================================================================


def test_warn_mode_returns_detections():
    config = ScannerConfig(mode="warn")
    scanner = SecretScanner(config)
    text = "key: AKIAIOSFODNN7EXAMPLE"
    detections = scanner.scan(text)
    assert len(detections) >= 1


def test_redact_mode_is_default():
    scanner = SecretScanner()
    assert scanner.config.mode == "warn"


def test_block_mode_config():
    config = ScannerConfig(mode="block")
    scanner = SecretScanner(config)
    assert scanner.config.mode == "block"
