use std::{
    ffi::OsStr,
    fs,
    path::Path,
    process::{Command, Output},
};

const EXPECTED_LOCAL_TEST_OUTPUT: &str = concat!(
    "test \"explicit local net invalid connect facade\" ... ok\n",
    "test \"explicit local net invalid listen facade\" ... ok\n",
    "test \"explicit local net invalid bound port facade\" ... ok\n",
    "test \"explicit local net invalid accept facade\" ... ok\n",
    "test \"explicit local net invalid read facade\" ... ok\n",
    "test \"explicit local net invalid write facade\" ... ok\n",
    "test \"explicit local net invalid close facade\" ... ok\n",
    "test \"explicit local net helper invalids facade\" ... ok\n",
    "test \"explicit local net facade all\" ... ok\n",
    "9 test(s) passed\n",
);

const EXPECTED_STD_IMPORT_TEST_OUTPUT: &str = concat!(
    "test \"explicit std net invalid connect facade\" ... ok\n",
    "test \"explicit std net invalid listen facade\" ... ok\n",
    "test \"explicit std net invalid bound port facade\" ... ok\n",
    "test \"explicit std net invalid accept facade\" ... ok\n",
    "test \"explicit std net invalid read facade\" ... ok\n",
    "test \"explicit std net invalid write facade\" ... ok\n",
    "test \"explicit std net invalid close facade\" ... ok\n",
    "test \"explicit std net helper invalids facade\" ... ok\n",
    "test \"explicit std net facade all\" ... ok\n",
    "9 test(s) passed\n",
);

const STANDARD_NET_SOURCE_FACADE_ALPHA: &[&str] = &[
    "tcp_connect_loopback_result",
    "tcp_listen_loopback_result",
    "tcp_bound_port_result",
    "tcp_accept_result",
    "tcp_read_all_result",
    "tcp_write_text_result",
    "tcp_close_result",
    "tcp_write_text_ok",
    "tcp_close_ok",
];

const STANDARD_NET_RUNTIME_NAMES: &[&str] = &[
    "std.net.tcp_connect_loopback_result",
    "std.net.tcp_listen_loopback_result",
    "std.net.tcp_bound_port_result",
    "std.net.tcp_accept_result",
    "std.net.tcp_read_all_result",
    "std.net.tcp_write_text_result",
    "std.net.tcp_close_result",
];

#[test]
fn standard_net_source_facade_project_checks_formats_and_tests() {
    let project =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-layout-local-net");

    assert_local_net_fixture_is_source_authored(&project);

    let fmt = run_glagol([
        OsStr::new("fmt"),
        OsStr::new("--check"),
        project.as_os_str(),
    ]);
    assert_success("std layout local net fmt --check", &fmt);

    let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
    assert_success_stdout(check, "", "std layout local net check");

    let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
    assert_success_stdout(
        test,
        EXPECTED_LOCAL_TEST_OUTPUT,
        "std layout local net test output",
    );
}

#[test]
fn standard_net_std_import_project_checks_formats_and_tests() {
    let project = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples/projects/std-import-net");

    assert_std_import_net_fixture_uses_repo_std(&project);

    let fmt = run_glagol([
        OsStr::new("fmt"),
        OsStr::new("--check"),
        project.as_os_str(),
    ]);
    assert_success("std import net fmt --check", &fmt);

    let check = run_glagol([OsStr::new("check"), project.as_os_str()]);
    assert_success_stdout(check, "", "std import net check");

    let test = run_glagol([OsStr::new("test"), project.as_os_str()]);
    assert_success_stdout(
        test,
        EXPECTED_STD_IMPORT_TEST_OUTPUT,
        "std import net test output",
    );
}

fn assert_local_net_fixture_is_source_authored(project: &Path) {
    let net = read(&project.join("src/net.slo"));
    let main = read(&project.join("src/main.slo"));

    assert!(
        net.starts_with("(module net (export "),
        "net.slo must stay an explicit local module export"
    );
    assert!(
        main.starts_with("(module main)\n\n(import net ("),
        "main.slo must stay an explicit local net import"
    );
    assert!(
        !main.contains("(import std") && !main.contains("(import slovo.std"),
        "net fixture must not depend on automatic or package std imports"
    );
    assert_net_source_shape(&net, &main, "local net fixture");
    assert!(
        !main.contains("std."),
        "local net main fixture must use only local imports"
    );
}

fn assert_std_import_net_fixture_uses_repo_std(project: &Path) {
    let std_net = read(&Path::new(env!("CARGO_MANIFEST_DIR")).join("../lib/std/net.slo"));
    let main = read(&project.join("src/main.slo"));

    assert!(
        !project.join("src/net.slo").exists(),
        "std import net fixture must use repo-root std/net.slo, not a local copy"
    );
    assert!(
        main.starts_with("(module main)\n\n(import std.net ("),
        "std import net fixture must use explicit `std.net` import syntax"
    );
    assert_net_source_shape(&std_net, &main, "repo std.net fixture");
}

fn assert_net_source_shape(net: &str, main: &str, context: &str) {
    for runtime_name in STANDARD_NET_RUNTIME_NAMES {
        assert!(
            net.contains(runtime_name),
            "{} must wrap `{}`",
            context,
            runtime_name
        );
    }
    assert_std_only_contains(net, STANDARD_NET_RUNTIME_NAMES, context);
    assert!(
        !net.contains("dns")
            && !net.contains("tls")
            && !net.contains("udp")
            && !net.contains("async")
            && !net.contains("http")
            && !net.contains("timeout")
            && !net.contains("non_loopback")
            && !net.contains("host_error")
            && !main.contains("dns")
            && !main.contains("tls")
            && !main.contains("udp")
            && !main.contains("async")
            && !main.contains("http")
            && !main.contains("timeout")
            && !main.contains("non_loopback")
            && !main.contains("host_error"),
        "{} must not claim deferred networking policies",
        context
    );

    for helper in STANDARD_NET_SOURCE_FACADE_ALPHA {
        assert!(
            net.contains(&format!("(fn {} ", helper)),
            "{} is missing source facade `{}`",
            context,
            helper
        );
        assert!(
            main.contains(helper),
            "{} main fixture import/use is missing `{}`",
            context,
            helper
        );
    }
}

fn assert_std_only_contains(source: &str, allowed: &[&str], context: &str) {
    let mut remaining = source.to_string();
    for name in allowed {
        remaining = remaining.replace(name, "");
    }
    assert!(
        !remaining.contains("std."),
        "{} introduced unexpected compiler-known std names",
        context
    );
}

fn run_glagol<I, S>(args: I) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<std::ffi::OsStr>,
{
    Command::new(env!("CARGO_BIN_EXE_glagol"))
        .args(args)
        .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")))
        .output()
        .expect("run glagol")
}

fn read(path: &Path) -> String {
    fs::read_to_string(path).unwrap_or_else(|err| panic!("read `{}`: {}", path.display(), err))
}

fn assert_success(context: &str, output: &Output) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "{} failed\nstdout:\n{}\nstderr:\n{}",
        context,
        stdout,
        stderr
    );
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}

fn assert_success_stdout(output: Output, expected: &str, context: &str) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "{} failed\nstdout:\n{}\nstderr:\n{}",
        context,
        stdout,
        stderr
    );
    assert_eq!(stdout, expected, "{} stdout drifted", context);
    assert!(stderr.is_empty(), "{} wrote stderr:\n{}", context, stderr);
}
