use std::{
    collections::BTreeSet,
    fs,
    path::{Path, PathBuf},
};

use anyhow::{Context, bail};

use super::board;
use crate::{
    context::{arch_for_target_checked, starry_target_for_arch_checked},
    test_qemu::validate_supported_target,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StarryTestGroup {
    Normal,
    Stress,
}

impl StarryTestGroup {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Normal => "normal",
            Self::Stress => "stress",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StarryQemuCase {
    pub(crate) name: String,
    pub(crate) qemu_config_path: PathBuf,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StarryBoardTestGroup {
    pub(crate) name: String,
    pub(crate) board_name: String,
    pub(crate) arch: String,
    pub(crate) target: String,
    pub(crate) build_config_path: PathBuf,
    pub(crate) board_test_config_path: PathBuf,
}

pub(crate) fn parse_test_target(
    workspace_root: &Path,
    target: &str,
) -> anyhow::Result<(String, String)> {
    let supported_targets = board::board_default_list(workspace_root)?
        .into_iter()
        .filter(|board| board.name.starts_with("qemu-"))
        .map(|board| board.target)
        .collect::<Vec<_>>();

    let supported_target_refs = supported_targets
        .iter()
        .map(String::as_str)
        .collect::<Vec<_>>();
    let supported_arches = supported_targets
        .iter()
        .map(|target| arch_for_target_checked(target))
        .collect::<anyhow::Result<BTreeSet<_>>>()?
        .into_iter()
        .collect::<Vec<_>>();

    if target.contains('-') {
        validate_supported_target(
            target,
            "starry qemu tests",
            "targets",
            &supported_target_refs,
        )?;
        Ok((
            arch_for_target_checked(target)?.to_string(),
            target.to_string(),
        ))
    } else {
        validate_supported_target(
            target,
            "starry qemu tests",
            "arch values",
            &supported_arches,
        )?;
        Ok((
            target.to_string(),
            starry_target_for_arch_checked(target)?.to_string(),
        ))
    }
}

pub(crate) fn discover_qemu_cases(
    workspace_root: &Path,
    arch: &str,
    selected_case: Option<&str>,
    group: StarryTestGroup,
) -> anyhow::Result<Vec<StarryQemuCase>> {
    let test_suite_dir = test_suite_dir(workspace_root, group);
    let config_name = qemu_config_name(arch);

    if let Some(case_name) = selected_case {
        let case_dir = test_suite_dir.join(case_name);
        if !case_dir.is_dir() {
            bail!(
                "unknown Starry {} test case `{case_name}` in {}; available cases are discovered \
                 from direct subdirectories",
                group.as_str(),
                test_suite_dir.display()
            );
        }

        let qemu_config_path = case_dir.join(&config_name);
        if !qemu_config_path.is_file() {
            bail!(
                "Starry test case `{case_name}` does not provide `{}`",
                qemu_config_path.display()
            );
        }

        return Ok(vec![StarryQemuCase {
            name: case_name.to_string(),
            qemu_config_path,
        }]);
    }

    let mut cases = fs::read_dir(&test_suite_dir)
        .with_context(|| format!("failed to read {}", test_suite_dir.display()))?
        .filter_map(|entry| entry.ok())
        .filter_map(|entry| {
            let path = entry.path();
            if !path.is_dir() {
                return None;
            }

            let name = entry.file_name().into_string().ok()?;
            let qemu_config_path = path.join(&config_name);
            if qemu_config_path.is_file() {
                Some(StarryQemuCase {
                    name,
                    qemu_config_path,
                })
            } else {
                None
            }
        })
        .collect::<Vec<_>>();
    cases.sort_by(|left, right| left.name.cmp(&right.name));

    if cases.is_empty() {
        bail!(
            "no Starry {} qemu test cases for arch `{arch}` found under {}",
            group.as_str(),
            test_suite_dir.display()
        );
    }

    Ok(cases)
}

pub(crate) fn finalize_qemu_case_run(
    failed: &[String],
    group: StarryTestGroup,
) -> anyhow::Result<()> {
    if failed.is_empty() {
        println!("all starry {} qemu test cases passed", group.as_str());
        Ok(())
    } else {
        bail!(
            "starry {} qemu tests failed for {} case(s): {}",
            group.as_str(),
            failed.len(),
            failed.join(", ")
        )
    }
}

pub(crate) fn discover_board_test_groups(
    workspace_root: &Path,
    selected_group: Option<&str>,
) -> anyhow::Result<Vec<StarryBoardTestGroup>> {
    let test_suite_dir = test_suite_dir(workspace_root, StarryTestGroup::Normal);
    let mut groups = collect_board_test_groups(workspace_root, &test_suite_dir)?;
    groups.sort_by(|left, right| left.name.cmp(&right.name));

    if let Some(group_name) = selected_group {
        return groups
            .into_iter()
            .find(|group| group.name == group_name)
            .map(|group| vec![group])
            .ok_or_else(|| {
                anyhow::anyhow!(
                    "unsupported Starry board test group `{group_name}`. Supported groups are: {}",
                    supported_board_test_group_names(workspace_root)
                        .unwrap_or_else(|_| "<none>".to_string())
                )
            });
    }

    if groups.is_empty() {
        bail!(
            "no Starry board test groups found under {}",
            test_suite_dir.display()
        );
    }

    Ok(groups)
}

pub(crate) fn finalize_board_test_run(failed: &[String]) -> anyhow::Result<()> {
    if failed.is_empty() {
        println!("all starry board test groups passed");
        Ok(())
    } else {
        bail!(
            "starry board tests failed for {} group(s): {}",
            failed.len(),
            failed.join(", ")
        )
    }
}

fn test_suite_dir(workspace_root: &Path, group: StarryTestGroup) -> PathBuf {
    workspace_root
        .join("test-suit")
        .join("starryos")
        .join(group.as_str())
}

fn qemu_config_name(arch: &str) -> String {
    format!("qemu-{arch}.toml")
}

fn collect_board_test_groups(
    workspace_root: &Path,
    test_suite_dir: &Path,
) -> anyhow::Result<Vec<StarryBoardTestGroup>> {
    let mut groups = Vec::new();
    for entry in fs::read_dir(test_suite_dir)
        .with_context(|| format!("failed to read {}", test_suite_dir.display()))?
    {
        let entry = entry?;
        let case_dir = entry.path();
        if !case_dir.is_dir() {
            continue;
        }

        let case_name = match entry.file_name().into_string() {
            Ok(name) => name,
            Err(_) => continue,
        };

        for config_entry in fs::read_dir(&case_dir)
            .with_context(|| format!("failed to read {}", case_dir.display()))?
        {
            let config_entry = config_entry?;
            let config_path = config_entry.path();
            if !config_path.is_file() || config_path.extension().is_none_or(|ext| ext != "toml") {
                continue;
            }

            let Some(stem) = config_path.file_stem().and_then(|stem| stem.to_str()) else {
                continue;
            };
            let Some(board_name) = stem.strip_prefix("board-") else {
                continue;
            };

            let build_config_path = workspace_root
                .join("os/StarryOS/configs/board")
                .join(format!("{board_name}.toml"));
            if !build_config_path.is_file() {
                bail!(
                    "Starry board test group `{}-{board_name}` maps to missing build config `{}`",
                    case_name,
                    build_config_path.display()
                );
            }

            let board_file = board::load_board_file(&build_config_path).with_context(|| {
                format!(
                    "failed to load mapped Starry build config for board test group \
                     `{}-{board_name}`",
                    case_name
                )
            })?;
            groups.push(StarryBoardTestGroup {
                name: format!("{case_name}-{board_name}"),
                board_name: board_name.to_string(),
                arch: arch_for_target_checked(&board_file.target)?.to_string(),
                target: board_file.target,
                build_config_path,
                board_test_config_path: config_path,
            });
        }
    }

    Ok(groups)
}

fn supported_board_test_group_names(workspace_root: &Path) -> anyhow::Result<String> {
    let test_suite_dir = test_suite_dir(workspace_root, StarryTestGroup::Normal);
    let mut groups = collect_board_test_groups(workspace_root, &test_suite_dir)?
        .into_iter()
        .map(|group| group.name)
        .collect::<Vec<_>>();
    groups.sort();
    Ok(groups.join(", "))
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::*;

    fn write_board_build_config(root: &Path, board_name: &str, target: &str) {
        let path = root
            .join("os/StarryOS/configs/board")
            .join(format!("{board_name}.toml"));
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(
            path,
            format!(
                "target = \"{target}\"\nenv = {{}}\nfeatures = [\"qemu\"]\nlog = \
                 \"Info\"\nplat_dyn = false\n"
            ),
        )
        .unwrap();
    }

    fn write_board_test_config(root: &Path, case_name: &str, board_name: &str) -> PathBuf {
        let path = root
            .join("test-suit/starryos/normal")
            .join(case_name)
            .join(format!("board-{board_name}.toml"));
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(
            &path,
            "board_type = \"OrangePi-5-Plus\"\nshell_prefix = \
             \"orangepi@orangepi5plus:~\"\nshell_init_cmd = \"pwd && echo 'test \
             pass'\"\nsuccess_regex = [\"(?m)^test pass\\\\s*$\"]\nfail_regex = []\ntimeout = \
             300\n",
        )
        .unwrap();
        path
    }

    #[test]
    fn discovers_board_test_group_and_build_mapping() {
        let root = tempdir().unwrap();
        write_board_build_config(
            root.path(),
            "orangepi-5-plus",
            "aarch64-unknown-none-softfloat",
        );
        let board_test_config = write_board_test_config(root.path(), "smoke", "orangepi-5-plus");

        let groups = discover_board_test_groups(root.path(), None).unwrap();

        assert_eq!(groups.len(), 1);
        assert_eq!(groups[0].name, "smoke-orangepi-5-plus");
        assert_eq!(groups[0].board_name, "orangepi-5-plus");
        assert_eq!(groups[0].arch, "aarch64");
        assert_eq!(groups[0].target, "aarch64-unknown-none-softfloat");
        assert_eq!(
            groups[0].build_config_path,
            root.path()
                .join("os/StarryOS/configs/board/orangepi-5-plus.toml")
        );
        assert_eq!(groups[0].board_test_config_path, board_test_config);
    }

    #[test]
    fn filters_board_test_group_by_name() {
        let root = tempdir().unwrap();
        write_board_build_config(
            root.path(),
            "orangepi-5-plus",
            "aarch64-unknown-none-softfloat",
        );
        write_board_build_config(root.path(), "vision-five2", "riscv64gc-unknown-none-elf");
        write_board_test_config(root.path(), "smoke", "orangepi-5-plus");
        write_board_test_config(root.path(), "smoke", "vision-five2");

        let groups =
            discover_board_test_groups(root.path(), Some("smoke-orangepi-5-plus")).unwrap();

        assert_eq!(groups.len(), 1);
        assert_eq!(groups[0].name, "smoke-orangepi-5-plus");
    }

    #[test]
    fn rejects_missing_mapped_board_build_config() {
        let root = tempdir().unwrap();
        write_board_test_config(root.path(), "smoke", "orangepi-5-plus");

        let err = discover_board_test_groups(root.path(), None)
            .unwrap_err()
            .to_string();

        assert!(err.contains("smoke-orangepi-5-plus"));
        assert!(err.contains("os/StarryOS/configs/board/orangepi-5-plus.toml"));
    }
}
