"""Tests for scanner modules and rule engine."""

from __future__ import annotations

import json
import plistlib
import tempfile
from pathlib import Path

import pytest

from oncecheck.engine.runner import Finding, ScanResult, run_scan
from oncecheck.engine.reporter import export_json, export_text, export_sarif
from oncecheck.scanners.detector import Platform, DetectionResult, detect_platforms


# ---------------------------------------------------------------------------
# Fixtures — create minimal project structures in temp dirs
# ---------------------------------------------------------------------------

@pytest.fixture
def ios_project(tmp_path):
    """Create a minimal iOS project that triggers known rules."""
    # Info.plist with ATS disabled, no camera key
    plist_data = {
        "CFBundleIdentifier": "com.example.testapp",
        "NSAppTransportSecurity": {"NSAllowsArbitraryLoads": True},
    }
    plist_path = tmp_path / "Info.plist"
    with open(plist_path, "wb") as f:
        plistlib.dump(plist_data, f)

    # Swift source using camera API
    src_dir = tmp_path / "Sources"
    src_dir.mkdir()
    (src_dir / "Camera.swift").write_text(
        'import AVFoundation\nlet session = AVCaptureSession()\n'
    )
    # HTTP URL in source
    (src_dir / "API.swift").write_text(
        'let url = "http://api.example.com/data"\n'
    )
    return tmp_path


@pytest.fixture
def ios_project_with_secrets(tmp_path):
    """iOS project with hardcoded secrets."""
    plist_data = {"CFBundleIdentifier": "com.example.testapp"}
    with open(tmp_path / "Info.plist", "wb") as f:
        plistlib.dump(plist_data, f)
    src_dir = tmp_path / "Sources"
    src_dir.mkdir()
    (src_dir / "Config.swift").write_text(
        'let apiKey = "sk_live_abcdefghijklmnopqrstuvwxyz1234"\n'
        'let session = URLSession.shared\n'
    )
    return tmp_path


@pytest.fixture
def android_project(tmp_path):
    """Create a minimal Android project that triggers known rules."""
    # build.gradle with low target SDK and compileSdk < targetSdk
    (tmp_path / "build.gradle").write_text(
        'android {\n'
        '    compileSdkVersion 30\n'
        '    defaultConfig {\n'
        '        targetSdkVersion 33\n'
        '    }\n'
        '}\n'
    )
    # AndroidManifest.xml with dangerous permissions and debuggable
    (tmp_path / "AndroidManifest.xml").write_text(
        '<?xml version="1.0" encoding="utf-8"?>\n'
        '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
        '    <uses-permission android:name="android.permission.CAMERA" />\n'
        '    <uses-permission android:name="android.permission.READ_SMS" />\n'
        '    <application android:debuggable="true">\n'
        '    </application>\n'
        '</manifest>\n'
    )
    return tmp_path


@pytest.fixture
def android_project_with_secrets(tmp_path):
    """Android project with hardcoded secrets."""
    (tmp_path / "build.gradle").write_text(
        'android { compileSdkVersion 34\n'
        '    defaultConfig { targetSdkVersion 34 }\n'
        '}\n'
    )
    (tmp_path / "AndroidManifest.xml").write_text(
        '<?xml version="1.0" encoding="utf-8"?>\n'
        '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
        '    <application></application>\n'
        '</manifest>\n'
    )
    src_dir = tmp_path / "java"
    src_dir.mkdir()
    (src_dir / "Config.java").write_text(
        'String apiKey = "AIzaSyAbcdefghijklmnopqrstuvwxyz123456";\n'
    )
    return tmp_path


@pytest.fixture
def web_project(tmp_path):
    """Create a minimal web project that triggers known rules."""
    # package.json + lockfile
    (tmp_path / "package.json").write_text('{"name": "test", "version": "1.0.0"}')
    (tmp_path / "package-lock.json").write_text("{}")

    # next.config.js (empty — no security headers)
    (tmp_path / "next.config.js").write_text("module.exports = {};")

    # Source with issues
    src_dir = tmp_path / "src"
    src_dir.mkdir()
    (src_dir / "page.tsx").write_text(
        '<div onClick={() => {}}>\n'
        '  <img src="photo.jpg" />\n'
        '</div>\n'
        '<script>eval("code")</script>\n'
        '<script src="https://www.googletagmanager.com/gtag.js"></script>\n'
    )
    return tmp_path


@pytest.fixture
def web_project_with_xss(tmp_path):
    """Web project with innerHTML/dangerouslySetInnerHTML usage."""
    (tmp_path / "package.json").write_text('{"name": "test", "version": "1.0.0"}')
    (tmp_path / "next.config.js").write_text("module.exports = {};")
    src_dir = tmp_path / "src"
    src_dir.mkdir()
    (src_dir / "page.tsx").write_text(
        'element.innerHTML = userInput;\n'
        '<div dangerouslySetInnerHTML={{__html: content}} />\n'
    )
    return tmp_path


@pytest.fixture
def clean_web_project(tmp_path):
    """A web project with no compliance issues."""
    (tmp_path / "package.json").write_text('{"name": "clean", "version": "1.0.0"}')
    (tmp_path / "next.config.js").write_text(
        'module.exports = { headers: () => [{ '
        'key: "Content-Security-Policy", value: "default-src self", '
        'key: "Strict-Transport-Security", value: "max-age=31536000", '
        'key: "X-Frame-Options", value: "DENY", '
        'key: "X-Content-Type-Options", value: "nosniff", '
        'key: "Referrer-Policy", value: "strict-origin", '
        'key: "Permissions-Policy", value: "camera=()" '
        '}] };'
    )
    pages_dir = tmp_path / "pages"
    pages_dir.mkdir()
    (pages_dir / "privacy-policy.tsx").write_text("<div>Privacy Policy</div>")
    (pages_dir / "terms.tsx").write_text("<div>Terms of Service</div>")
    (pages_dir / "index.tsx").write_text(
        '<button onClick={() => {}}>Click</button>\n'
        '<img alt="photo" src="photo.jpg" />\n'
    )
    return tmp_path


# ---------------------------------------------------------------------------
# Detector tests
# ---------------------------------------------------------------------------

class TestDetector:
    def test_detect_ios(self, ios_project):
        results = detect_platforms(ios_project)
        platforms = [r.platform for r in results]
        assert Platform.IOS in platforms

    def test_detect_android(self, android_project):
        results = detect_platforms(android_project)
        platforms = [r.platform for r in results]
        assert Platform.ANDROID in platforms

    def test_detect_web(self, web_project):
        results = detect_platforms(web_project)
        platforms = [r.platform for r in results]
        assert Platform.WEB in platforms

    def test_detect_empty_dir(self, tmp_path):
        results = detect_platforms(tmp_path)
        assert results == []


# ---------------------------------------------------------------------------
# iOS scanner tests
# ---------------------------------------------------------------------------

class TestIOSScanner:
    def test_catches_missing_camera_desc(self, ios_project):
        from oncecheck.scanners.ios import scan
        findings = scan(ios_project)
        ids = [f.rule_id for f in findings]
        assert "IOS-PRIV-001" in ids

    def test_catches_ats_disabled(self, ios_project):
        from oncecheck.scanners.ios import scan
        findings = scan(ios_project)
        ids = [f.rule_id for f in findings]
        assert "IOS-SEC-001" in ids

    def test_catches_http_urls(self, ios_project):
        from oncecheck.scanners.ios import scan
        findings = scan(ios_project)
        ids = [f.rule_id for f in findings]
        assert "IOS-SEC-002" in ids

    def test_catches_missing_privacy_manifest(self, ios_project):
        from oncecheck.scanners.ios import scan
        findings = scan(ios_project)
        ids = [f.rule_id for f in findings]
        assert "IOS-PRIV-007" in ids

    def test_catches_hardcoded_secrets(self, ios_project_with_secrets):
        from oncecheck.scanners.ios import scan
        findings = scan(ios_project_with_secrets)
        ids = [f.rule_id for f in findings]
        assert "IOS-SEC-003" in ids

    def test_catches_missing_ssl_pinning(self, ios_project_with_secrets):
        from oncecheck.scanners.ios import scan
        findings = scan(ios_project_with_secrets)
        ids = [f.rule_id for f in findings]
        assert "IOS-SEC-004" in ids

    def test_catches_keychain_usage(self, tmp_path):
        """Detects sensitive data in UserDefaults instead of Keychain."""
        plist_data = {"CFBundleIdentifier": "com.example.testapp"}
        with open(tmp_path / "Info.plist", "wb") as f:
            plistlib.dump(plist_data, f)
        src_dir = tmp_path / "Sources"
        src_dir.mkdir()
        (src_dir / "Auth.swift").write_text(
            'UserDefaults.standard.set(token, forKey: "password")\n'
        )
        from oncecheck.scanners.ios import scan
        findings = scan(tmp_path)
        ids = [f.rule_id for f in findings]
        assert "IOS-SEC-005" in ids

    def test_catches_healthkit_missing_desc(self, tmp_path):
        """Detects HealthKit usage without NSHealthShareUsageDescription."""
        plist_data = {"CFBundleIdentifier": "com.example.testapp"}
        with open(tmp_path / "Info.plist", "wb") as f:
            plistlib.dump(plist_data, f)
        src_dir = tmp_path / "Sources"
        src_dir.mkdir()
        (src_dir / "Health.swift").write_text(
            'let store = HKHealthStore()\n'
        )
        from oncecheck.scanners.ios import scan
        findings = scan(tmp_path)
        ids = [f.rule_id for f in findings]
        assert "IOS-PRIV-009" in ids

    def test_all_findings_have_fix(self, ios_project):
        from oncecheck.scanners.ios import scan
        findings = scan(ios_project)
        for f in findings:
            assert f.fix, f"Finding {f.rule_id} has no fix"


# ---------------------------------------------------------------------------
# Android scanner tests
# ---------------------------------------------------------------------------

class TestAndroidScanner:
    def test_catches_low_target_sdk(self, android_project):
        from oncecheck.scanners.android import scan
        findings = scan(android_project)
        ids = [f.rule_id for f in findings]
        assert "AND-SDK-001" in ids

    def test_catches_compile_sdk_mismatch(self, android_project):
        from oncecheck.scanners.android import scan
        findings = scan(android_project)
        ids = [f.rule_id for f in findings]
        assert "AND-SDK-002" in ids

    def test_catches_dangerous_permissions(self, android_project):
        from oncecheck.scanners.android import scan
        findings = scan(android_project)
        ids = [f.rule_id for f in findings]
        assert "AND-PERM-001" in ids

    def test_catches_sms_permission(self, android_project):
        from oncecheck.scanners.android import scan
        findings = scan(android_project)
        ids = [f.rule_id for f in findings]
        assert "AND-PERM-003" in ids

    def test_catches_debuggable(self, android_project):
        from oncecheck.scanners.android import scan
        findings = scan(android_project)
        ids = [f.rule_id for f in findings]
        assert "AND-SEC-001" in ids

    def test_catches_hardcoded_secrets(self, android_project_with_secrets):
        from oncecheck.scanners.android import scan
        findings = scan(android_project_with_secrets)
        ids = [f.rule_id for f in findings]
        assert "AND-DATA-002" in ids

    def test_catches_missing_proguard(self, tmp_path):
        """Detects missing ProGuard/R8 obfuscation in release build."""
        (tmp_path / "build.gradle").write_text(
            'android {\n'
            '    compileSdkVersion 34\n'
            '    defaultConfig { targetSdkVersion 34 }\n'
            '    buildTypes {\n'
            '        release {\n'
            '            minifyEnabled false\n'
            '        }\n'
            '    }\n'
            '}\n'
        )
        (tmp_path / "AndroidManifest.xml").write_text(
            '<?xml version="1.0" encoding="utf-8"?>\n'
            '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
            '    <application></application>\n'
            '</manifest>\n'
        )
        from oncecheck.scanners.android import scan
        findings = scan(tmp_path)
        ids = [f.rule_id for f in findings]
        assert "AND-SEC-005" in ids

    def test_catches_fg_service_no_type(self, tmp_path):
        """Detects foreground service without foregroundServiceType."""
        (tmp_path / "build.gradle").write_text(
            'android { compileSdkVersion 34\n'
            '    defaultConfig { targetSdkVersion 34 }\n'
            '}\n'
        )
        (tmp_path / "AndroidManifest.xml").write_text(
            '<?xml version="1.0" encoding="utf-8"?>\n'
            '<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
            '    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />\n'
            '    <application>\n'
            '        <service android:name=".MyService" />\n'
            '    </application>\n'
            '</manifest>\n'
        )
        from oncecheck.scanners.android import scan
        findings = scan(tmp_path)
        ids = [f.rule_id for f in findings]
        assert "AND-POL-003" in ids

    def test_all_findings_have_fix(self, android_project):
        from oncecheck.scanners.android import scan
        findings = scan(android_project)
        for f in findings:
            assert f.fix, f"Finding {f.rule_id} has no fix"


# ---------------------------------------------------------------------------
# Web scanner tests
# ---------------------------------------------------------------------------

class TestWebScanner:
    def test_catches_missing_csp(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-SEC-001" in ids

    def test_catches_eval_usage(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-OWASP-002" in ids

    def test_catches_img_no_alt(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-A11Y-001" in ids

    def test_catches_div_onclick(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-A11Y-002" in ids

    def test_catches_tracking_no_consent(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-PRIV-001" in ids

    def test_catches_missing_privacy_policy(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-PRIV-002" in ids

    def test_catches_missing_x_content_type(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-SEC-004" in ids

    def test_catches_innerHTML_xss(self, web_project_with_xss):
        from oncecheck.scanners.web import scan
        findings = scan(web_project_with_xss)
        ids = [f.rule_id for f in findings]
        assert "WEB-OWASP-003" in ids

    def test_catches_missing_sri(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-OWASP-005" in ids

    def test_catches_cors_wildcard(self, tmp_path):
        """Detects CORS wildcard origin misconfiguration."""
        (tmp_path / "package.json").write_text('{"name": "test", "version": "1.0.0"}')
        (tmp_path / "server.js").write_text(
            'app.use(cors());\n'
            'res.header("Access-Control-Allow-Origin", "*");\n'
        )
        from oncecheck.scanners.web import scan
        findings = scan(tmp_path)
        ids = [f.rule_id for f in findings]
        assert "WEB-SEC-008" in ids

    def test_catches_missing_rate_limiting(self, web_project):
        from oncecheck.scanners.web import scan
        findings = scan(web_project)
        ids = [f.rule_id for f in findings]
        assert "WEB-SEC-010" in ids


# ---------------------------------------------------------------------------
# Common (cross-platform) scanner tests
# ---------------------------------------------------------------------------

class TestCommonScanner:
    def test_catches_deprecated_apis(self, tmp_path):
        """Detects deprecated API usage."""
        src_dir = tmp_path / "src"
        src_dir.mkdir()
        (src_dir / "App.jsx").write_text(
            'class MyComponent extends React.Component {\n'
            '    componentWillMount() { console.log("mounting"); }\n'
            '}\n'
        )
        from oncecheck.scanners.common import scan
        findings = scan(tmp_path)
        ids = [f.rule_id for f in findings]
        assert "SUPPLY-API-001" in ids

    def test_all_findings_have_fix(self, tmp_path):
        """All common scanner findings have a fix."""
        src_dir = tmp_path / "src"
        src_dir.mkdir()
        (src_dir / "App.jsx").write_text('const x = 1;\n')
        from oncecheck.scanners.common import scan
        findings = scan(tmp_path)
        for f in findings:
            assert f.fix, f"Finding {f.rule_id} has no fix"


# ---------------------------------------------------------------------------
# Rule engine tests
# ---------------------------------------------------------------------------

class TestRuleEngine:
    def test_run_scan_ios(self, ios_project):
        detection = DetectionResult(Platform.IOS, ios_project, "SwiftUI", 0.9)
        result = run_scan(detection)
        assert isinstance(result, ScanResult)
        assert result.platform == Platform.IOS
        assert result.scan_time_seconds >= 0
        assert result.fail_count > 0

    def test_run_scan_android(self, android_project):
        detection = DetectionResult(Platform.ANDROID, android_project, "Gradle", 0.9)
        result = run_scan(detection)
        assert isinstance(result, ScanResult)
        assert result.fail_count > 0

    def test_scan_result_to_dict(self, ios_project):
        detection = DetectionResult(Platform.IOS, ios_project, "SwiftUI", 0.9)
        result = run_scan(detection)
        d = result.to_dict()
        assert d["platform"] == "ios"
        assert "findings" in d
        assert len(d["findings"]) > 0


# ---------------------------------------------------------------------------
# Reporter tests
# ---------------------------------------------------------------------------

class TestReporter:
    def test_export_json(self, ios_project):
        detection = DetectionResult(Platform.IOS, ios_project, "SwiftUI", 0.9)
        result = run_scan(detection)
        text = export_json([result])
        data = json.loads(text)
        assert "scans" in data
        assert len(data["scans"]) == 1

    def test_export_text(self, ios_project):
        detection = DetectionResult(Platform.IOS, ios_project, "SwiftUI", 0.9)
        result = run_scan(detection)
        text = export_text([result])
        assert "FAIL" in text or "WARN" in text

    def test_export_sarif(self, ios_project):
        detection = DetectionResult(Platform.IOS, ios_project, "SwiftUI", 0.9)
        result = run_scan(detection)
        text = export_sarif([result])
        data = json.loads(text)
        assert data["version"] == "2.1.0"
        assert len(data["runs"]) == 1

    def test_export_json_to_file(self, ios_project, tmp_path):
        detection = DetectionResult(Platform.IOS, ios_project, "SwiftUI", 0.9)
        result = run_scan(detection)
        out = tmp_path / "results.json"
        export_json([result], out)
        assert out.exists()
        data = json.loads(out.read_text())
        assert "scans" in data

    def test_export_path_validation(self, ios_project):
        detection = DetectionResult(Platform.IOS, ios_project, "SwiftUI", 0.9)
        result = run_scan(detection)
        with pytest.raises(ValueError):
            export_json([result], "/etc/passwd")


# ---------------------------------------------------------------------------
# Token store tests
# ---------------------------------------------------------------------------

class TestTokenStore:
    def test_save_and_load(self, tmp_path, monkeypatch):
        from oncecheck.auth import token_store
        monkeypatch.setattr(token_store, "_token_dir", lambda: tmp_path)
        token_store.save_token({"access_token": "test123", "plan": "pro"})
        loaded = token_store.load_token()
        assert loaded is not None
        assert loaded["access_token"] == "test123"
        assert "issued_at" in loaded

    def test_expired_token(self, tmp_path, monkeypatch):
        import time
        from oncecheck.auth import token_store
        monkeypatch.setattr(token_store, "_token_dir", lambda: tmp_path)
        token_store.save_token({"access_token": "old", "issued_at": time.time() - 999999})
        loaded = token_store.load_token()
        assert loaded is None

    def test_clear_token(self, tmp_path, monkeypatch):
        from oncecheck.auth import token_store
        monkeypatch.setattr(token_store, "_token_dir", lambda: tmp_path)
        token_store.save_token({"access_token": "test"})
        token_store.clear_token()
        assert token_store.load_token() is None
