// Rust treats all test modules as independent crates
// Therefore it does not see that these utility functions are actually used
#![allow(dead_code)]
use std::collections::HashMap;
use std::{env, fs, io::Read, path::PathBuf};

use assert_cmd::{Command, cargo};
use korrect::ToK8sStr;
use korrect::config::{DownloadMode, VersionStrategy};
use korrect::test_utils::{TestEnv, path_containing_korrect, setup_testenv};
use mockito::{Matcher, Server};
use semver::Version;
use serde_json::Value;

pub fn assert_on_korrect_list(korrect_home: &PathBuf, list_items: &[&str]) {
    let home = korrect_home.to_str().unwrap();
    let output = cargo::cargo_bin_cmd!("korrect")
        .env_clear()
        .arg("list")
        .env("HOME", home)
        .output()
        .expect("failed to execute korrect list");

    assert!(output.status.success());
    let bullet_lines = list_items
        .iter()
        .map(|item| format!("- {}", item))
        .collect::<Vec<_>>()
        .join("\n");
    let expected = format!(
        "Installed components in {}/.korrect/bin:\n{}\n",
        home, bullet_lines
    );
    assert_on_outputs(output, None, Some(expected));
}

pub fn mock_kubeconfig() -> String {
    let path = fixture_file("kubeconfig");
    let file =
        fs::read_to_string(&path).unwrap_or_else(|_| panic!("Failed to open {}", path.display()));
    file
}

fn fixture_file(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join(name)
}

pub fn read_fixture_binary(name: &str) -> Vec<u8> {
    let path = fixture_file(name);
    let mut file =
        fs::File::open(&path).unwrap_or_else(|_| panic!("Failed to open {}", path.display()));
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer).expect("Failed to read file");
    buffer
}

fn generate_helm_tags(major: Option<u32>, minor: Vec<u32>, patch: Vec<u32>) -> String {
    let major = major.unwrap_or(3);
    let mut tags = vec![];
    for m in minor.into_iter() {
        for p in patch.clone().into_iter() {
            tags.push(format!(r#"{{"name": "v{}.{}.{}"}}"#, major, m, p));
        }
    }
    format!("[{}]", tags.join(", "))
}

/// Serves the mock binaries from a local http server so that the korrect-shim can request versions and download mock binaries
pub fn start_mock_binary_server(
    latest_kubectl_version: Option<&str>,
    latest_helm_version: Option<&str>,
    kubectl_minor_versions: Option<&Vec<Version>>,
) -> mockito::ServerGuard {
    let kubectl_binary_data = read_fixture_binary("mock_kubectl");
    let helm_binary_data = read_fixture_binary("mock_helm.tar.gz");
    let kubectl_stable_content = latest_kubectl_version.unwrap_or("v1.33.1");
    let helm_stable_content = latest_helm_version.unwrap_or("v3.18.2");
    let helm_tags = generate_helm_tags(None, vec![16, 17, 18], vec![0, 1, 2, 3, 4, 5]);
    let mut server = Server::new();
    server
        .mock("GET", Matcher::Regex(r"/helm-v.*".to_string()))
        .with_status(200)
        .with_header("Content-Type", "application/x-tar")
        .with_header("content-length", &helm_binary_data.len().to_string())
        .with_body(&helm_binary_data)
        .create();

    server
        .mock("GET", "/helm-latest-version")
        .with_status(200)
        .with_header("content-type", "text/plain")
        .with_body(helm_stable_content)
        .create();

    server
        .mock("GET", "/repos/helm/helm/tags")
        .with_status(200)
        .with_header("content-type", "application/json; charset=utf-8")
        .with_body(helm_tags)
        .create();

    server
        .mock(
            "GET",
            Matcher::Regex(r"/release/.*/bin/.*/.*/kubectl".to_string()),
        )
        .with_status(200)
        .with_header("Content-Type", "application/octet-stream")
        .with_header("content-length", &kubectl_binary_data.len().to_string())
        .with_body(&kubectl_binary_data)
        .create();

    // Allows us to simulate the kubectl stable minor version responses of kubectl
    let mut hashmap: HashMap<String, String> = std::collections::HashMap::new();
    if let Some(versions) = kubectl_minor_versions {
        for version in versions {
            hashmap.insert(
                format!("stable-1.{}.txt", version.minor),
                version.to_k8s_str(),
            );
        }
    } else {
        for num in 1..=50 {
            hashmap.insert(format!("stable-1.{}.txt", num), format!("v1.{}.2", num));
        }
    }
    hashmap.insert("stable.txt".to_owned(), kubectl_stable_content.to_owned());
    for (a, b) in hashmap {
        server
            .mock("GET", Matcher::Regex(format!("/release/{}", a).to_string()))
            .with_status(200)
            .with_header("content-type", "text/plain")
            .with_body(b)
            .create();
    }

    log::debug!("Download Server running at {}", &server.url());
    server
}

pub fn assert_setup_korrect_failure(
    test_env: Option<&TestEnv>,
    download_mode: Option<DownloadMode>,
    version_strategy: Option<VersionStrategy>,
) -> TestEnv {
    let (test_env, mut cmd) =
        exec_korrect("setup", test_env, download_mode, version_strategy, vec![]);
    cmd.assert().failure();
    test_env
}

pub fn force_setup_korrect(
    test_env: Option<&TestEnv>,
    download_mode: Option<DownloadMode>,
    version_strategy: Option<VersionStrategy>,
) -> TestEnv {
    let mut extra_args = vec![];
    extra_args.push("--force".into());
    let (test_env, mut cmd) = exec_korrect(
        "setup",
        test_env,
        download_mode,
        version_strategy,
        extra_args,
    );
    cmd.assert().success();
    test_env
}

pub fn setup_korrect(
    test_env: Option<&TestEnv>,
    download_mode: Option<DownloadMode>,
    version_strategy: Option<VersionStrategy>,
) -> TestEnv {
    let (test_env, mut cmd) =
        exec_korrect("setup", test_env, download_mode, version_strategy, vec![]);
    cmd.assert().success();
    test_env
}

pub fn uninstall_korrect(
    test_env: Option<&TestEnv>,
    download_mode: Option<DownloadMode>,
    version_strategy: Option<VersionStrategy>,
) -> TestEnv {
    let (test_env, mut cmd) = exec_korrect(
        "uninstall",
        test_env,
        download_mode,
        version_strategy,
        vec![],
    );
    cmd.assert().success();
    test_env
}

pub fn force_uninstall_korrect(
    test_env: Option<&TestEnv>,
    download_mode: Option<DownloadMode>,
    version_strategy: Option<VersionStrategy>,
) -> TestEnv {
    let mut extra_args = vec![];
    extra_args.push("--force".into());
    let (test_env, mut cmd) = exec_korrect(
        "uninstall",
        test_env,
        download_mode,
        version_strategy,
        extra_args,
    );
    cmd.assert().success();
    test_env
}

fn exec_korrect(
    command: &str,
    test_env: Option<&TestEnv>,
    download_mode: Option<DownloadMode>,
    version_strategy: Option<VersionStrategy>,
    extra_args: Vec<String>,
) -> (TestEnv, Command) {
    let test_env = match test_env {
        None => setup_testenv(),
        Some(test_env) => test_env.clone(),
    };
    let mut cmd = cargo::cargo_bin_cmd!("korrect");
    cmd.env_clear()
        .arg(command)
        .env("HOME", &test_env.home)
        .env(
            "RUST_LOG",
            std::env::var("TEST_RUST_LOG").unwrap_or_default(),
        );

    if let Some(mode) = download_mode {
        cmd.arg("--download-mode").arg(mode.to_string());
    }
    if let Some(mode) = version_strategy {
        cmd.arg("--version-strategy").arg(mode.to_string());
    }
    for arg in extra_args {
        cmd.arg(arg);
    }
    return (test_env, cmd);
}

pub fn run_cmd_with_env(
    cmd_str: &str,
    test_env: &TestEnv,
    base_url: Option<String>,
) -> std::process::Output {
    let korrect_bin_path = &test_env.home.join(".korrect").join("bin");
    let mut cmd = Command::new("bash");
    cmd.env_clear()
        .arg("-c")
        .arg(cmd_str)
        .env("PATH", path_containing_korrect(korrect_bin_path))
        .env("HOME", &test_env.home)
        // NOTE: I'm not sure this is actually needed
        .env(
            "RUST_LOG",
            std::env::var("TEST_RUST_LOG").unwrap_or_default(),
        );

    if let Some(url) = base_url {
        log::debug!("Setting env KORRECT_BASE_URL:{}", url);
        cmd.env("KORRECT_BASE_URL", url);
    }

    for (key, value) in &test_env.env_vars {
        log::debug!("Setting env {}:{}", key, value);
        cmd.env(key, value);
    }

    // println!("%",cmd);
    let result = cmd.output().expect("failed to execute command");
    println!("{:?}", result);
    result
}

pub fn assert_on_cache(cache_file: &PathBuf) {
    assert!(
        cache_file.exists(),
        "{} does not exist",
        cache_file.display()
    );

    let contents = fs::read_to_string(cache_file)
        .unwrap_or_else(|_| panic!("Could not read {}", cache_file.display()));
    let json: Value = serde_json::from_str(&contents)
        .unwrap_or_else(|_| panic!("Invalid JSON in {}", cache_file.display()));

    for key in ["ttl", "version"] {
        assert!(
            json.get(key).is_some(),
            "Missing key '{}'. File Contents: {}",
            key,
            contents
        );
    }
}

pub fn assert_on_outputs(
    output: std::process::Output,
    expected_stderr: Option<String>,
    expected_stdout: Option<String>,
) {
    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();

    check_output(expected_stderr, "stderr", stderr);
    check_output(expected_stdout, "stdout", stdout);
}

fn check_output(expected: Option<String>, output_name: &str, actual: String) {
    match expected {
        Some(expected) => {
            assert!(
                actual.contains(&expected),
                "Unexpected {} output. Got:\n{}\nExpected:\n{}",
                output_name,
                actual,
                expected
            );
        }
        None => {
            assert!(
                actual.is_empty(),
                "Unexpected {} output:\n{}",
                output_name,
                actual
            )
        }
    }
}
