"""Suite 7 — Information leakage analysis."""

from __future__ import annotations
import asyncio
import re
from typing import Any

import httpx

from .http_client import send_request
from .payloads import INFO_LEAK_PATHS, INFO_LEAK_PATTERNS


# Sensitive headers that should not be exposed
_SENSITIVE_HEADERS = {
    "x-powered-by": ("HIGH", "CWE-200", "Server technology exposed via X-Powered-By"),
    "server": ("MEDIUM", "CWE-200", "Server software version exposed"),
    "x-aspnet-version": ("HIGH", "CWE-200", "ASP.NET version exposed"),
    "x-aspnetmvc-version": ("HIGH", "CWE-200", "ASP.NET MVC version exposed"),
    "x-debug-token": ("CRITICAL", "CWE-200", "Debug token exposed in production"),
    "x-debug-token-link": ("CRITICAL", "CWE-200", "Debug endpoint exposed in production"),
}


async def run_info_leak_tests(
    client: httpx.AsyncClient,
    url: str,
    method: str,
    headers: dict[str, str] | None,
    body: Any | None,
    mode: str = "full",
) -> list:
    from .runner import TestResult

    results: list[TestResult] = []

    # ── 1. Probe known sensitive paths ──
    from urllib.parse import urlparse
    parsed = urlparse(url)
    base = f"{parsed.scheme}://{parsed.netloc}"

    paths_to_check = INFO_LEAK_PATHS if mode == "full" else INFO_LEAK_PATHS[:6]

    for path in paths_to_check:
        probe_url = base + path
        r = await send_request(client, url=probe_url, method="GET", headers=None, body=None)
        code = r["status_code"]

        if code and 200 <= code < 300:
            resp_body = r.get("body", "") or ""
            # Check if it returned actual content (not just a generic error page)
            if len(resp_body) > 10:
                results.append(TestResult(
                    test=f"Info leak: {path}",
                    status="CRASH",
                    detail=f"Sensitive path accessible ({code}) — {len(resp_body)} bytes returned",
                    latency_ms=r["latency_ms"], category="info_leak",
                    severity="CRITICAL", cwe="CWE-200",
                ))
            else:
                results.append(TestResult(
                    test=f"Info leak: {path}",
                    status="WARN",
                    detail=f"Path returned {code} but minimal content",
                    latency_ms=r["latency_ms"], category="info_leak",
                    severity="MEDIUM", cwe="CWE-200",
                ))
        elif code and code == 403:
            results.append(TestResult(
                test=f"Info leak: {path}",
                status="WARN",
                detail="Path exists but access forbidden (confirms file exists)",
                latency_ms=r["latency_ms"], category="info_leak",
                severity="LOW", cwe="CWE-200",
            ))
        else:
            results.append(TestResult(
                test=f"Info leak: {path}",
                status="PASS",
                detail=f"Not accessible ({code})",
                latency_ms=r["latency_ms"], category="info_leak",
                severity="INFO", cwe="CWE-200",
            ))

    # ── 2. Trigger error and scan response body for leaks ──
    # Send a deliberately bad request to trigger an error response
    error_triggers = [
        ("Nonexistent path", base + "/____apistrike_nonexistent_404____"),
        ("Invalid ID", url + "/999999999999"),
        ("SQL error trigger", url + "/'"),
    ]

    for label, trigger_url in error_triggers:
        r = await send_request(client, url=trigger_url, method="GET", headers=headers, body=None)
        resp_body = r.get("body", "") or ""

        for pattern, pattern_name, cwe in INFO_LEAK_PATTERNS:
            matches = re.findall(pattern, resp_body, re.IGNORECASE)
            if matches:
                sample = matches[0] if isinstance(matches[0], str) else str(matches[0])
                if len(sample) > 80:
                    sample = sample[:80] + "…"
                results.append(TestResult(
                    test=f"Leak [{label}]: {pattern_name}",
                    status="CRASH" if "credential" in pattern_name.lower() or "debug" in pattern_name.lower() else "WARN",
                    detail=f"Detected in error response: {sample}",
                    latency_ms=r["latency_ms"], category="info_leak",
                    severity="CRITICAL" if "credential" in pattern_name.lower() else "HIGH", cwe=cwe,
                ))

    # ── 3. Analyze response headers from the original endpoint ──
    r = await send_request(client, url=url, method=method, headers=headers, body=body)
    resp_headers = r.get("headers", {})

    for hdr_name, (sev, cwe, desc) in _SENSITIVE_HEADERS.items():
        hdr_val = resp_headers.get(hdr_name)
        if hdr_val:
            results.append(TestResult(
                test=f"Header leak: {hdr_name}",
                status="WARN",
                detail=f"{desc}: {hdr_val}",
                latency_ms=r["latency_ms"], category="info_leak",
                severity=sev, cwe=cwe,
            ))

    # Check for missing security headers
    security_headers = {
        "strict-transport-security": ("MEDIUM", "CWE-319", "Missing HSTS header"),
        "x-content-type-options": ("LOW", "CWE-16", "Missing X-Content-Type-Options"),
        "x-frame-options": ("LOW", "CWE-1021", "Missing X-Frame-Options"),
        "content-security-policy": ("LOW", "CWE-16", "Missing Content-Security-Policy"),
    }

    for hdr_name, (sev, cwe, desc) in security_headers.items():
        if hdr_name not in resp_headers:
            results.append(TestResult(
                test=f"Missing header: {hdr_name}",
                status="WARN",
                detail=desc,
                latency_ms=0, category="info_leak",
                severity=sev, cwe=cwe,
            ))

    # ── 4. CORS misconfiguration check ──
    h = dict(headers or {})
    h["Origin"] = "https://evil-attacker.com"
    r = await send_request(client, url=url, method=method, headers=h, body=body)
    acao = r.get("headers", {}).get("access-control-allow-origin", "")
    if acao == "*" or "evil-attacker.com" in acao:
        results.append(TestResult(
            test="CORS misconfiguration",
            status="CRASH",
            detail=f"Server reflects arbitrary Origin in ACAO: {acao}",
            latency_ms=r["latency_ms"], category="info_leak",
            severity="HIGH", cwe="CWE-942",
        ))
    elif acao:
        results.append(TestResult(
            test="CORS configuration",
            status="PASS",
            detail=f"ACAO: {acao}",
            latency_ms=r["latency_ms"], category="info_leak",
            severity="INFO", cwe="CWE-942",
        ))

    # ── 5. CORS with null origin ──
    h_null = dict(headers or {})
    h_null["Origin"] = "null"
    r = await send_request(client, url=url, method=method, headers=h_null, body=body)
    acao_null = r.get("headers", {}).get("access-control-allow-origin", "")
    if acao_null == "null":
        results.append(TestResult(
            test="CORS null origin accepted",
            status="CRASH",
            detail="Server accepts null Origin — sandbox bypass possible",
            latency_ms=r["latency_ms"], category="info_leak",
            severity="HIGH", cwe="CWE-942",
        ))

    # ── 6. CORS credentials with wildcard ──
    acac = r.get("headers", {}).get("access-control-allow-credentials", "")
    if acac.lower() == "true" and acao in ("*", "https://evil-attacker.com"):
        results.append(TestResult(
            test="CORS credentials with wildcard",
            status="CRASH",
            detail="Allow-Credentials: true with permissive origin — cookie theft possible",
            latency_ms=r["latency_ms"], category="info_leak",
            severity="CRITICAL", cwe="CWE-942",
        ))

    if mode == "quick":
        return results

    # ── 7. Version fingerprinting via errors ──
    fingerprint_urls = [
        base + "/nonexistent_" + str(hash("fp"))[:8],
        base + "/api/v999/nope",
        base + "/favicon.ico",
    ]
    for fp_url in fingerprint_urls:
        r = await send_request(client, url=fp_url, method="GET", headers=None, body=None)
        resp_body = r.get("body", "") or ""
        resp_headers = r.get("headers", {})
        # Check for framework-specific error pages
        fp_patterns = [
            ("nginx", r"nginx/[\d.]+", "Nginx version exposed"),
            ("apache", r"Apache/[\d.]+", "Apache version exposed"),
            ("express", r"Cannot (?:GET|POST|PUT|DELETE)", "Express.js framework detected"),
            ("django", r"(?:Django|CSRF verification failed)", "Django framework detected"),
            ("flask", r"(?:Werkzeug|flask)", "Flask framework detected"),
            ("asp.net", r"(?:ASP\.NET|__VIEWSTATE)", "ASP.NET detected"),
            ("tomcat", r"Apache Tomcat/[\d.]+", "Tomcat version exposed"),
            ("spring", r"Whitelabel Error Page", "Spring Boot detected"),
            ("laravel", r"(?:Laravel|Illuminate)", "Laravel framework detected"),
        ]
        for name, pattern, desc in fp_patterns:
            if re.search(pattern, resp_body, re.IGNORECASE):
                results.append(TestResult(
                    test=f"Fingerprint: {name}",
                    status="WARN",
                    detail=desc,
                    latency_ms=r["latency_ms"], category="info_leak",
                    severity="MEDIUM", cwe="CWE-200",
                ))
        # Also check Server header
        server_hdr = resp_headers.get("server", "")
        if server_hdr and any(v in server_hdr.lower() for v in ["nginx", "apache", "iis", "tomcat", "gunicorn", "uvicorn"]):
            results.append(TestResult(
                test=f"Server header: {server_hdr[:50]}",
                status="WARN",
                detail=f"Server software disclosed: {server_hdr}",
                latency_ms=r["latency_ms"], category="info_leak",
                severity="MEDIUM", cwe="CWE-200",
            ))
            break

    # ── 8. Cookie security analysis ──
    r = await send_request(client, url=url, method=method, headers=headers, body=body)
    resp_headers = r.get("headers", {})
    set_cookie = resp_headers.get("set-cookie", "")
    if set_cookie:
        if "httponly" not in set_cookie.lower():
            results.append(TestResult(
                test="Cookie missing HttpOnly",
                status="WARN",
                detail="Set-Cookie without HttpOnly flag — XSS can steal cookies",
                latency_ms=0, category="info_leak",
                severity="HIGH", cwe="CWE-1004",
            ))
        if "secure" not in set_cookie.lower():
            results.append(TestResult(
                test="Cookie missing Secure flag",
                status="WARN",
                detail="Set-Cookie without Secure flag — sent over HTTP",
                latency_ms=0, category="info_leak",
                severity="MEDIUM", cwe="CWE-614",
            ))
        if "samesite" not in set_cookie.lower():
            results.append(TestResult(
                test="Cookie missing SameSite",
                status="WARN",
                detail="Set-Cookie without SameSite attribute — CSRF risk",
                latency_ms=0, category="info_leak",
                severity="MEDIUM", cwe="CWE-1275",
            ))

    # ── 9. Sensitive path probing in parallel ──
    sem = asyncio.Semaphore(15)
    extra_paths = INFO_LEAK_PATHS[20:] if len(INFO_LEAK_PATHS) > 20 else []

    async def _probe_path(path):
        async with sem:
            probe_url = base + path
            r = await send_request(client, url=probe_url, method="GET", headers=None, body=None)
            code = r["status_code"]
            if code and 200 <= code < 300:
                resp_body = r.get("body", "") or ""
                if len(resp_body) > 10:
                    return TestResult(
                        test=f"Info leak: {path}",
                        status="CRASH",
                        detail=f"Sensitive path accessible ({code}) — {len(resp_body)} bytes",
                        latency_ms=r["latency_ms"], category="info_leak",
                        severity="CRITICAL" if any(k in path for k in [".env", ".git", "key", "pem", "shadow", "passwd"]) else "HIGH",
                        cwe="CWE-200",
                    )
            elif code and code == 403:
                return TestResult(
                    test=f"Info leak: {path}",
                    status="WARN",
                    detail="Path exists but forbidden (confirms resource exists)",
                    latency_ms=r["latency_ms"], category="info_leak",
                    severity="LOW", cwe="CWE-200",
                )
            return None

    extra_results = await asyncio.gather(*[_probe_path(p) for p in extra_paths])
    results.extend([r for r in extra_results if r is not None])

    return results
