use std::collections::HashMap;
use std::env;
use std::fs;
use std::io::{self, BufRead, BufReader, Read, Write};
use std::iter::Peekable;
use std::mem;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::{Chars, FromStr};
use std::thread;

use crate::config::{Color, Config, EmitMode, FileName, NewlineStyle};
use crate::formatting::{ReportedErrors, SourceFile};
use crate::rustfmt_diff::{DiffLine, Mismatch, ModifiedChunk, OutputWriter, make_diff, print_diff};
use crate::source_file;
use crate::{
    Edition, FormatReport, FormatReportFormatterBuilder, Input, Session, StyleEdition, Version,
    is_nightly_channel,
};

use rustfmt_config_proc_macro::nightly_only_test;
use tracing::{debug, warn};

mod configuration_snippet;
mod mod_resolver;
mod parser;

const DIFF_CONTEXT_SIZE: usize = 3;

// A list of files on which we want to skip testing.
const FILE_SKIP_LIST: &[&str] = &[
    // We want to make sure that the `skip_children` is correctly working,
    // so we do not want to test this file directly.
    "configs/skip_children/foo/mod.rs",
    "issue-3434/no_entry.rs",
    "issue-3665/sub_mod.rs",
    // Testing for issue-3779
    "issue-3779/ice.rs",
    // These files and directory are a part of modules defined inside `cfg_if!`.
    "cfg_if/mod.rs",
    "cfg_if/detect",
    "issue-3253/foo.rs",
    "issue-3253/bar.rs",
    "issue-3253/paths",
    // These files and directory are a part of modules defined inside `cfg_attr(..)`.
    "cfg_mod/dir",
    "cfg_mod/bar.rs",
    "cfg_mod/foo.rs",
    "cfg_mod/wasm32.rs",
    "skip/foo.rs",
];

fn init_log() {
    let _ = tracing_subscriber::fmt().with_test_writer().try_init();
}

struct TestSetting {
    /// The size of the stack of the thread that run tests.
    stack_size: usize,
}

impl Default for TestSetting {
    fn default() -> Self {
        TestSetting {
            stack_size: 8_388_608, // 8MB
        }
    }
}

fn run_test_with<F>(test_setting: &TestSetting, f: F)
where
    F: FnOnce(),
    F: Send + 'static,
{
    thread::Builder::new()
        .stack_size(test_setting.stack_size)
        .spawn(f)
        .expect("Failed to create a test thread")
        .join()
        .expect("Failed to join a test thread")
}

fn is_subpath<P>(path: &Path, subpath: &P) -> bool
where
    P: AsRef<Path>,
{
    (0..path.components().count())
        .map(|i| {
            path.components()
                .skip(i)
                .take(subpath.as_ref().components().count())
        })
        .any(|c| c.zip(subpath.as_ref().components()).all(|(a, b)| a == b))
}

fn is_file_skip(path: &Path) -> bool {
    FILE_SKIP_LIST
        .iter()
        .any(|file_path| is_subpath(path, file_path))
}

// Returns a `Vec` containing `PathBuf`s of files with an  `rs` extension in the
// given path. The `recursive` argument controls if files from subdirectories
// are also returned.
fn get_test_files(path: &Path, recursive: bool) -> Vec<PathBuf> {
    let mut files = vec![];
    if path.is_dir() {
        for entry in
            fs::read_dir(path).expect(&format!("couldn't read directory {}", path.display()))
        {
            let entry = entry.expect("couldn't get `DirEntry`");
            let path = entry.path();
            if path.is_dir() && recursive {
                files.append(&mut get_test_files(&path, recursive));
            } else if path.extension().map_or(false, |f| f == "rs") && !is_file_skip(&path) {
                files.push(path);
            }
        }
    }
    files
}

fn verify_config_used(path: &Path, config_name: &str) {
    for entry in fs::read_dir(path).expect(&format!("couldn't read {} directory", path.display())) {
        let entry = entry.expect("couldn't get directory entry");
        let path = entry.path();
        if path.extension().map_or(false, |f| f == "rs") {
            // check if "// rustfmt-<config_name>:" appears in the file.
            let filebuf = BufReader::new(
                fs::File::open(&path)
                    .unwrap_or_else(|_| panic!("couldn't read file {}", path.display())),
            );
            assert!(
                filebuf
                    .lines()
                    .map(Result::unwrap)
                    .take_while(|l| l.starts_with("//"))
                    .any(|l| l.starts_with(&format!("// rustfmt-{}", config_name))),
                "config option file {} does not contain expected config name",
                path.display()
            );
        }
    }
}

#[test]
fn verify_config_test_names() {
    init_log();
    for path in &[
        Path::new("tests/source/configs"),
        Path::new("tests/target/configs"),
    ] {
        for entry in fs::read_dir(path).expect("couldn't read configs directory") {
            let entry = entry.expect("couldn't get directory entry");
            let path = entry.path();
            if path.is_dir() {
                let config_name = path.file_name().unwrap().to_str().unwrap();

                // Make sure that config name is used in the files in the directory.
                verify_config_used(&path, config_name);
            }
        }
    }
}

// This writes to the terminal using the same approach (via `term::stdout` or
// `println!`) that is used by `rustfmt::rustfmt_diff::print_diff`. Writing
// using only one or the other will cause the output order to differ when
// `print_diff` selects the approach not used.
fn write_message(msg: &str) {
    let mut writer = OutputWriter::new(Color::Auto);
    writer.writeln(msg, None);
}

// Integration tests. The files in `tests/source` are formatted and compared
// to their equivalent in `tests/target`. The target file and config can be
// overridden by annotations in the source file. The input and output must match
// exactly.
#[test]
fn system_tests() {
    init_log();
    run_test_with(&TestSetting::default(), || {
        // Get all files in the tests/source directory.
        let files = get_test_files(Path::new("tests/source"), true);
        let (_reports, count, fails) = check_files(files, &None);

        // Display results.
        println!("Ran {} system tests.", count);
        assert_eq!(fails, 0, "{} system tests failed", fails);
        assert!(
            count >= 300,
            "Expected a minimum of {} system tests to be executed",
            300
        )
    });
}

// Do the same for tests/coverage-source directory.
// The only difference is the coverage mode.
#[test]
fn coverage_tests() {
    init_log();
    let files = get_test_files(Path::new("tests/coverage/source"), true);
    let (_reports, count, fails) = check_files(files, &None);

    println!("Ran {count} tests in coverage mode.");
    assert_eq!(fails, 0, "{fails} tests failed");
}

#[test]
fn checkstyle_test() {
    init_log();
    let filename = "tests/writemode/source/fn-single-line.rs";
    let expected_filename = "tests/writemode/target/checkstyle.xml";
    assert_output(Path::new(filename), Path::new(expected_filename));
}

#[test]
fn json_test() {
    init_log();
    let filename = "tests/writemode/source/json.rs";
    let expected_filename = "tests/writemode/target/output.json";
    assert_output(Path::new(filename), Path::new(expected_filename));
}

#[test]
fn modified_test() {
    init_log();
    use std::io::BufRead;

    // Test "modified" output
    let filename = "tests/writemode/source/modified.rs";
    let mut data = Vec::new();
    let mut config = Config::default();
    config
        .set()
        .emit_mode(crate::config::EmitMode::ModifiedLines);

    {
        let mut session = Session::new(config, Some(&mut data));
        session.format(Input::File(filename.into())).unwrap();
    }

    let mut lines = data.lines();
    let mut chunks = Vec::new();
    while let Some(Ok(header)) = lines.next() {
        // Parse the header line
        let values: Vec<_> = header
            .split(' ')
            .map(|s| s.parse::<u32>().unwrap())
            .collect();
        assert_eq!(values.len(), 3);
        let line_number_orig = values[0];
        let lines_removed = values[1];
        let num_added = values[2];
        let mut added_lines = Vec::new();
        for _ in 0..num_added {
            added_lines.push(lines.next().unwrap().unwrap());
        }
        chunks.push(ModifiedChunk {
            line_number_orig,
            lines_removed,
            lines: added_lines,
        });
    }

    assert_eq!(
        chunks,
        vec![
            ModifiedChunk {
                line_number_orig: 4,
                lines_removed: 4,
                lines: vec!["fn blah() {}".into()],
            },
            ModifiedChunk {
                line_number_orig: 9,
                lines_removed: 6,
                lines: vec!["#[cfg(a, b)]".into(), "fn main() {}".into()],
            },
        ],
    );
}

// Helper function for comparing the results of rustfmt
// to a known output file generated by one of the write modes.
fn assert_output(source: &Path, expected_filename: &Path) {
    let config = read_config(source);
    let (_, source_file, _) = format_file(source, config.clone());

    // Populate output by writing to a vec.
    let mut out = vec![];
    let _ = source_file::write_all_files(&source_file, &mut out, &config);
    let output = String::from_utf8(out).unwrap();

    let mut expected_file = fs::File::open(&expected_filename).expect("couldn't open target");
    let mut expected_text = String::new();
    expected_file
        .read_to_string(&mut expected_text)
        .expect("Failed reading target");

    let compare = make_diff(&expected_text, &output, DIFF_CONTEXT_SIZE);
    if !compare.is_empty() {
        let mut failures = HashMap::new();
        failures.insert(source.to_owned(), compare);
        print_mismatches_default_message(failures);
        panic!("Text does not match expected output");
    }
}

// Helper function for comparing the results of rustfmt
// to a known output generated by one of the write modes.
fn assert_stdin_output(
    source: &Path,
    expected_filename: &Path,
    emit_mode: EmitMode,
    has_diff: bool,
) {
    let mut config = Config::default();
    config.set().newline_style(NewlineStyle::Unix);
    config.set().emit_mode(emit_mode);

    let mut source_file = fs::File::open(&source).expect("couldn't open source");
    let mut source_text = String::new();
    source_file
        .read_to_string(&mut source_text)
        .expect("Failed reading target");
    let input = Input::Text(source_text);

    // Populate output by writing to a vec.
    let mut buf: Vec<u8> = vec![];
    {
        let mut session = Session::new(config, Some(&mut buf));
        session.format(input).unwrap();
        let errors = ReportedErrors {
            has_diff,
            ..Default::default()
        };
        assert_eq!(session.errors, errors);
    }

    let mut expected_file = fs::File::open(&expected_filename).expect("couldn't open target");
    let mut expected_text = String::new();
    expected_file
        .read_to_string(&mut expected_text)
        .expect("Failed reading target");

    let output = String::from_utf8(buf).unwrap();
    let compare = make_diff(&expected_text, &output, DIFF_CONTEXT_SIZE);
    if !compare.is_empty() {
        let mut failures = HashMap::new();
        failures.insert(source.to_owned(), compare);
        print_mismatches_default_message(failures);
        panic!("Text does not match expected output");
    }
}
// Idempotence tests. Files in tests/target are checked to be unaltered by
// rustfmt.
#[nightly_only_test]
#[test]
fn idempotence_tests() {
    init_log();
    run_test_with(&TestSetting::default(), || {
        // Get all files in the tests/target directory.
        let files = get_test_files(Path::new("tests/target"), true);
        let (_reports, count, fails) = check_files(files, &None);

        // Display results.
        println!("Ran {} idempotent tests.", count);
        assert_eq!(fails, 0, "{} idempotent tests failed", fails);
        assert!(
            count >= 400,
            "Expected a minimum of {} idempotent tests to be executed",
            400
        )
    });
}

// Run rustfmt on itself. This operation must be idempotent. We also check that
// no warnings are emitted.
// Issue-3443: these tests require nightly
#[nightly_only_test]
#[test]
fn self_tests() {
    init_log();
    let mut files = get_test_files(Path::new("tests"), false);
    let bin_directories = vec!["cargo-fmt", "git-rustfmt", "bin", "format-diff"];
    for dir in bin_directories {
        let mut path = PathBuf::from("src");
        path.push(dir);
        path.push("main.rs");
        files.push(path);
    }
    // for crates that need to be included but lies outside src
    let external_crates = vec!["check_diff", "config_proc_macro"];
    for external_crate in external_crates {
        let mut path = PathBuf::from(external_crate);
        path.push("src");
        let directory = fs::read_dir(&path).unwrap();
        let search_files = directory.filter_map(|file| {
            file.ok().and_then(|f| {
                let name = f.file_name();
                if matches!(name.as_os_str().to_str(), Some("main.rs" | "lib.rs")) {
                    Some(f.path())
                } else {
                    None
                }
            })
        });
        for file in search_files {
            files.push(file);
        }
    }
    files.push(PathBuf::from("src/lib.rs"));

    let (reports, count, fails) = check_files(files, &Some(PathBuf::from("rustfmt.toml")));
    let mut warnings = 0;

    // Display results.
    println!("Ran {count} self tests.");
    assert_eq!(fails, 0, "{fails} self tests failed");

    for format_report in reports {
        println!(
            "{}",
            FormatReportFormatterBuilder::new(&format_report).build()
        );
        warnings += format_report.warning_count();
    }

    assert_eq!(warnings, 0, "Rustfmt's code generated {warnings} warnings");
}

#[test]
fn format_files_find_new_files_via_cfg_if() {
    init_log();
    run_test_with(&TestSetting::default(), || {
        // To repro issue-4656, it is necessary that these files are parsed
        // as a part of the same session (hence this separate test runner).
        let files = vec![
            Path::new("tests/source/issue-4656/lib2.rs"),
            Path::new("tests/source/issue-4656/lib.rs"),
        ];

        let config = Config::default();
        let mut session = Session::<io::Stdout>::new(config, None);

        let mut write_result = HashMap::new();
        for file in files {
            assert!(file.exists());
            let result = session.format(Input::File(file.into())).unwrap();
            assert!(!session.has_formatting_errors());
            assert!(!result.has_warnings());
            let mut source_file = SourceFile::new();
            mem::swap(&mut session.source_file, &mut source_file);

            for (filename, text) in source_file {
                if let FileName::Real(ref filename) = filename {
                    write_result.insert(filename.to_owned(), text);
                }
            }
        }
        assert_eq!(
            3,
            write_result.len(),
            "Should have uncovered an extra file (format_me_please.rs) via lib.rs"
        );
        assert!(handle_result(write_result, None).is_ok());
    });
}

#[test]
fn stdin_formatting_smoke_test() {
    init_log();
    let input = Input::Text("fn main () {}".to_owned());
    let mut config = Config::default();
    config.set().emit_mode(EmitMode::Stdout);
    let mut buf: Vec<u8> = vec![];
    {
        let mut session = Session::new(config, Some(&mut buf));
        session.format(input).unwrap();
        assert!(session.has_no_errors());
    }

    #[cfg(not(windows))]
    assert_eq!(buf, "<stdin>:\n\nfn main() {}\n".as_bytes());
    #[cfg(windows)]
    assert_eq!(buf, "<stdin>:\n\nfn main() {}\r\n".as_bytes());
}

#[test]
fn stdin_parser_panic_caught() {
    init_log();
    // See issue #3239.
    for text in ["{", "}"].iter().cloned().map(String::from) {
        let mut buf = vec![];
        let mut session = Session::new(Default::default(), Some(&mut buf));
        let _ = session.format(Input::Text(text));

        assert!(session.has_parsing_errors());
    }
}

/// Ensures that `EmitMode::ModifiedLines` works with input from `stdin`. Useful
/// when embedding Rustfmt (e.g. inside RLS).
#[test]
fn stdin_works_with_modified_lines() {
    init_log();
    let input = "\nfn\n some( )\n{\n}\nfn main () {}\n";
    let output = "1 6 2\nfn some() {}\nfn main() {}\n";

    let input = Input::Text(input.to_owned());
    let mut config = Config::default();
    config.set().newline_style(NewlineStyle::Unix);
    config.set().emit_mode(EmitMode::ModifiedLines);
    let mut buf: Vec<u8> = vec![];
    {
        let mut session = Session::new(config, Some(&mut buf));
        session.format(input).unwrap();
        let errors = ReportedErrors {
            has_diff: true,
            ..Default::default()
        };
        assert_eq!(session.errors, errors);
    }
    assert_eq!(buf, output.as_bytes());
}

/// Ensures that `EmitMode::Json` works with input from `stdin`.
#[test]
fn stdin_works_with_json() {
    init_log();
    assert_stdin_output(
        Path::new("tests/writemode/source/stdin.rs"),
        Path::new("tests/writemode/target/stdin.json"),
        EmitMode::Json,
        true,
    );
}

/// Ensures that `EmitMode::Checkstyle` works with input from `stdin`.
#[test]
fn stdin_works_with_checkstyle() {
    init_log();
    assert_stdin_output(
        Path::new("tests/writemode/source/stdin.rs"),
        Path::new("tests/writemode/target/stdin.xml"),
        EmitMode::Checkstyle,
        false,
    );
}

#[test]
fn stdin_disable_all_formatting_test() {
    init_log();
    let input = String::from("fn main() { println!(\"This should not be formatted.\"); }");
    let mut child = Command::new(rustfmt().to_str().unwrap())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .arg("--config-path=./tests/config/disable_all_formatting.toml")
        .spawn()
        .expect("failed to execute child");

    {
        let stdin = child.stdin.as_mut().expect("failed to get stdin");
        stdin
            .write_all(input.as_bytes())
            .expect("failed to write stdin");
    }

    let output = child.wait_with_output().expect("failed to wait on child");
    assert!(output.status.success());
    assert!(output.stderr.is_empty());
    assert_eq!(input, String::from_utf8(output.stdout).unwrap());
}

#[test]
fn stdin_generated_files_issue_5172() {
    init_log();
    let input = Input::Text("//@generated\nfn   main() {}".to_owned());
    let mut config = Config::default();
    config.set().emit_mode(EmitMode::Stdout);
    config.set().format_generated_files(false);
    config.set().newline_style(NewlineStyle::Unix);
    let mut buf: Vec<u8> = vec![];
    {
        let mut session = Session::new(config, Some(&mut buf));
        session.format(input).unwrap();
        assert!(session.has_no_errors());
    }
    // N.B. this should be changed once `format_generated_files` is supported with stdin
    assert_eq!(
        String::from_utf8(buf).unwrap(),
        "<stdin>:\n\n//@generated\nfn main() {}\n",
    );
}

#[test]
fn stdin_handles_mod_inner_ignore_attr() {
    // see https://github.com/rust-lang/rustfmt/issues/5368
    init_log();
    let input = String::from("#![rustfmt::skip]\n\nfn    main() {  }");
    let mut child = Command::new(rustfmt().to_str().unwrap())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .expect("failed to execute child");

    {
        let stdin = child.stdin.as_mut().expect("failed to get stdin");
        stdin
            .write_all(input.as_bytes())
            .expect("failed to write stdin");
    }

    let output = child.wait_with_output().expect("failed to wait on child");
    assert!(output.status.success());
    assert!(output.stderr.is_empty());
    assert_eq!(input, String::from_utf8(output.stdout).unwrap());
}

#[test]
fn format_lines_errors_are_reported() {
    init_log();
    let long_identifier = String::from_utf8(vec![b'a'; 239]).unwrap();
    let input = Input::Text(format!("fn {long_identifier}() {{}}"));
    let mut config = Config::default();
    config.set().error_on_line_overflow(true);
    let mut session = Session::<io::Stdout>::new(config, None);
    session.format(input).unwrap();
    assert!(session.has_formatting_errors());
}

#[test]
fn format_lines_errors_are_reported_with_tabs() {
    init_log();
    let long_identifier = String::from_utf8(vec![b'a'; 97]).unwrap();
    let input = Input::Text(format!("fn a() {{\n\t{long_identifier}\n}}"));
    let mut config = Config::default();
    config.set().error_on_line_overflow(true);
    config.set().hard_tabs(true);
    let mut session = Session::<io::Stdout>::new(config, None);
    session.format(input).unwrap();
    assert!(session.has_formatting_errors());
}

// For each file, run rustfmt and collect the output.
// Returns the number of files checked and the number of failures.
fn check_files(files: Vec<PathBuf>, opt_config: &Option<PathBuf>) -> (Vec<FormatReport>, u32, u32) {
    let mut count = 0;
    let mut fails = 0;
    let mut reports = vec![];

    for file_name in files {
        let sig_comments = read_significant_comments(&file_name);
        if sig_comments.contains_key("unstable") && !is_nightly_channel!() {
            debug!(
                "Skipping '{}' because it requires unstable \
                 features which are only available on nightly...",
                file_name.display()
            );
            continue;
        }

        debug!("Testing '{}'...", file_name.display());

        match idempotent_check(&file_name, opt_config) {
            Ok(ref report) if report.has_warnings() => {
                print!("{}", FormatReportFormatterBuilder::new(report).build());
                fails += 1;
            }
            Ok(report) => reports.push(report),
            Err(err) => {
                if let IdempotentCheckError::Mismatch(msg) = err {
                    print_mismatches_default_message(msg);
                }
                fails += 1;
            }
        }

        count += 1;
    }

    (reports, count, fails)
}

fn print_mismatches_default_message(result: HashMap<PathBuf, Vec<Mismatch>>) {
    for (file_name, diff) in result {
        let mismatch_msg_formatter =
            |line_num| format!("\nMismatch at {}:{}:", file_name.display(), line_num);
        print_diff(diff, &mismatch_msg_formatter, &Default::default());
    }

    if let Some(mut t) = term::stdout() {
        t.reset().unwrap_or(());
    }
}

fn print_mismatches<T: Fn(u32) -> String>(
    result: HashMap<PathBuf, Vec<Mismatch>>,
    mismatch_msg_formatter: T,
) {
    for (_file_name, diff) in result {
        print_diff(diff, &mismatch_msg_formatter, &Default::default());
    }

    if let Some(mut t) = term::stdout() {
        t.reset().unwrap_or(());
    }
}

fn read_config(filename: &Path) -> Config {
    let sig_comments = read_significant_comments(filename);
    let (edition, style_edition, version) = get_editions_from_comments(&sig_comments);
    // Look for a config file. If there is a 'config' property in the significant comments, use
    // that. Otherwise, if there are no significant comments at all, look for a config file with
    // the same name as the test file.
    let mut config = if !sig_comments.is_empty() {
        get_config(
            sig_comments.get("config").map(Path::new),
            edition,
            style_edition,
            version,
        )
    } else {
        get_config(
            filename.with_extension("toml").file_name().map(Path::new),
            edition,
            style_edition,
            version,
        )
    };

    for (key, val) in &sig_comments {
        if key != "target" && key != "config" && key != "unstable" {
            config.override_value(key, val);
            if config.is_default(key) {
                warn!("Default value {} used explicitly for {}", val, key);
            }
        }
    }

    config
}

fn format_file<P: Into<PathBuf>>(filepath: P, config: Config) -> (bool, SourceFile, FormatReport) {
    let filepath = filepath.into();
    let input = Input::File(filepath);
    let mut session = Session::<io::Stdout>::new(config, None);
    let result = session.format(input).unwrap();
    let parsing_errors = session.has_parsing_errors();
    let mut source_file = SourceFile::new();
    mem::swap(&mut session.source_file, &mut source_file);
    (parsing_errors, source_file, result)
}

enum IdempotentCheckError {
    Mismatch(HashMap<PathBuf, Vec<Mismatch>>),
    Parse,
}

fn get_editions_from_comments(
    comments: &HashMap<String, String>,
) -> (Option<Edition>, Option<StyleEdition>, Option<Version>) {
    (
        comments
            .get("edition")
            .map(|e| Edition::from_str(e).expect(&format!("invalid edition value: '{}'", e))),
        comments.get("style_edition").map(|se| {
            StyleEdition::from_str(se).expect(&format!("invalid style_edition value: '{}'", se))
        }),
        comments
            .get("version")
            .map(|v| Version::from_str(v).expect(&format!("invalid version value: '{}'", v))),
    )
}

fn idempotent_check(
    filename: &PathBuf,
    opt_config: &Option<PathBuf>,
) -> Result<FormatReport, IdempotentCheckError> {
    let sig_comments = read_significant_comments(filename);
    let config = if let Some(ref config_file_path) = opt_config {
        let (edition, style_edition, version) = get_editions_from_comments(&sig_comments);
        Config::from_toml_path(config_file_path, edition, style_edition, version)
            .expect("`rustfmt.toml` not found")
    } else {
        read_config(filename)
    };
    let (parsing_errors, source_file, format_report) = format_file(filename, config);
    if parsing_errors {
        return Err(IdempotentCheckError::Parse);
    }

    let mut write_result = HashMap::new();
    for (filename, text) in source_file {
        if let FileName::Real(ref filename) = filename {
            write_result.insert(filename.to_owned(), text);
        }
    }

    let target = sig_comments.get("target").map(|x| &(*x)[..]);

    handle_result(write_result, target).map(|_| format_report)
}

// Reads test config file using the supplied (optional) file name. If there's no file name or the
// file doesn't exist, just return the default config. Otherwise, the file must be read
// successfully.
fn get_config(
    config_file: Option<&Path>,
    edition: Option<Edition>,
    style_edition: Option<StyleEdition>,
    version: Option<Version>,
) -> Config {
    let config_file_name = match config_file {
        None => return Config::default_for_possible_style_edition(style_edition, edition, version),
        Some(file_name) => {
            let mut full_path = PathBuf::from("tests/config/");
            full_path.push(file_name);
            if !full_path.exists() {
                return Config::default_for_possible_style_edition(style_edition, edition, version);
            };
            full_path
        }
    };

    let mut def_config_file = fs::File::open(config_file_name).expect("couldn't open config");
    let mut def_config = String::new();
    def_config_file
        .read_to_string(&mut def_config)
        .expect("Couldn't read config");

    Config::from_toml_for_style_edition(
        &def_config,
        Path::new("tests/config/"),
        edition,
        style_edition,
        version,
    )
    .expect("invalid TOML")
}

// Reads significant comments of the form: `// rustfmt-key: value` into a hash map.
fn read_significant_comments(file_name: &Path) -> HashMap<String, String> {
    let file = fs::File::open(file_name)
        .unwrap_or_else(|_| panic!("couldn't read file {}", file_name.display()));
    let reader = BufReader::new(file);
    let pattern = r"^\s*//\s*rustfmt-([^:]+):\s*(\S+)";
    let regex = regex::Regex::new(pattern).expect("failed creating pattern 1");

    // Matches lines containing significant comments or whitespace.
    let line_regex = regex::Regex::new(r"(^\s*$)|(^\s*//\s*rustfmt-[^:]+:\s*\S+)")
        .expect("failed creating pattern 2");

    reader
        .lines()
        .map(|line| line.expect("failed getting line"))
        .filter(|line| line_regex.is_match(line))
        .filter_map(|line| {
            regex.captures_iter(&line).next().map(|capture| {
                (
                    capture
                        .get(1)
                        .expect("couldn't unwrap capture")
                        .as_str()
                        .to_owned(),
                    capture
                        .get(2)
                        .expect("couldn't unwrap capture")
                        .as_str()
                        .to_owned(),
                )
            })
        })
        .collect()
}

// Compares output to input.
// TODO: needs a better name, more explanation.
fn handle_result(
    result: HashMap<PathBuf, String>,
    target: Option<&str>,
) -> Result<(), IdempotentCheckError> {
    let mut failures = HashMap::new();

    for (file_name, fmt_text) in result {
        // If file is in tests/source, compare to file with same name in tests/target.
        let target = get_target(&file_name, target);
        let open_error = format!("couldn't open target {target:?}");
        let mut f = fs::File::open(&target).expect(&open_error);

        let mut text = String::new();
        let read_error = format!("failed reading target {target:?}");
        f.read_to_string(&mut text).expect(&read_error);

        // Ignore LF and CRLF difference for Windows.
        if !string_eq_ignore_newline_repr(&fmt_text, &text) {
            if std::env::var_os("RUSTC_BLESS").is_some_and(|v| v != "0") {
                std::fs::write(target, fmt_text).unwrap();
                continue;
            }
            let diff = make_diff(&text, &fmt_text, DIFF_CONTEXT_SIZE);
            assert!(
                !diff.is_empty(),
                "Empty diff? Maybe due to a missing a newline at the end of a file?"
            );
            failures.insert(file_name, diff);
        }
    }

    if failures.is_empty() {
        Ok(())
    } else {
        Err(IdempotentCheckError::Mismatch(failures))
    }
}

// Maps source file paths to their target paths.
fn get_target(file_name: &Path, target: Option<&str>) -> PathBuf {
    if let Some(n) = file_name
        .components()
        .position(|c| c.as_os_str() == "source")
    {
        let mut target_file_name = PathBuf::new();
        for (i, c) in file_name.components().enumerate() {
            if i == n {
                target_file_name.push("target");
            } else {
                target_file_name.push(c.as_os_str());
            }
        }
        if let Some(replace_name) = target {
            target_file_name.with_file_name(replace_name)
        } else {
            target_file_name
        }
    } else {
        // This is either and idempotence check or a self check.
        file_name.to_owned()
    }
}

#[test]
fn rustfmt_diff_make_diff_tests() {
    init_log();
    let diff = make_diff("a\nb\nc\nd", "a\ne\nc\nd", 3);
    assert_eq!(
        diff,
        vec![Mismatch {
            line_number: 1,
            line_number_orig: 1,
            lines: vec![
                DiffLine::Context("a".into()),
                DiffLine::Resulting("b".into()),
                DiffLine::Expected("e".into()),
                DiffLine::Context("c".into()),
                DiffLine::Context("d".into()),
            ],
        }]
    );
}

#[test]
fn rustfmt_diff_no_diff_test() {
    init_log();
    let diff = make_diff("a\nb\nc\nd", "a\nb\nc\nd", 3);
    assert_eq!(diff, vec![]);
}

// Compare strings without distinguishing between CRLF and LF
fn string_eq_ignore_newline_repr(left: &str, right: &str) -> bool {
    let left = CharsIgnoreNewlineRepr(left.chars().peekable());
    let right = CharsIgnoreNewlineRepr(right.chars().peekable());
    left.eq(right)
}

struct CharsIgnoreNewlineRepr<'a>(Peekable<Chars<'a>>);

impl<'a> Iterator for CharsIgnoreNewlineRepr<'a> {
    type Item = char;

    fn next(&mut self) -> Option<char> {
        self.0.next().map(|c| {
            if c == '\r' {
                if *self.0.peek().unwrap_or(&'\0') == '\n' {
                    self.0.next();
                    '\n'
                } else {
                    '\r'
                }
            } else {
                c
            }
        })
    }
}

#[test]
fn string_eq_ignore_newline_repr_test() {
    init_log();
    assert!(string_eq_ignore_newline_repr("", ""));
    assert!(!string_eq_ignore_newline_repr("", "abc"));
    assert!(!string_eq_ignore_newline_repr("abc", ""));
    assert!(string_eq_ignore_newline_repr("a\nb\nc\rd", "a\nb\r\nc\rd"));
    assert!(string_eq_ignore_newline_repr("a\r\n\r\n\r\nb", "a\n\n\nb"));
    assert!(!string_eq_ignore_newline_repr("a\r\nbcd", "a\nbcdefghijk"));
}

struct TempFile {
    path: PathBuf,
}

fn make_temp_file(file_name: &'static str) -> TempFile {
    use std::env::var;
    use std::fs::File;

    // Used in the Rust build system.
    let target_dir = var("RUSTFMT_TEST_DIR").unwrap_or_else(|_| ".".to_owned());
    let path = Path::new(&target_dir).join(file_name);

    let mut file = File::create(&path).expect("couldn't create temp file");
    let content = "fn main() {}\n";
    file.write_all(content.as_bytes())
        .expect("couldn't write temp file");
    TempFile { path }
}

impl Drop for TempFile {
    fn drop(&mut self) {
        use std::fs::remove_file;
        remove_file(&self.path).expect("couldn't delete temp file");
    }
}

fn rustfmt() -> PathBuf {
    let mut me = env::current_exe().expect("failed to get current executable");
    // Chop of the test name.
    me.pop();
    // Chop off `deps`.
    me.pop();

    me.push("rustfmt");
    assert!(
        me.is_file() || me.with_extension("exe").is_file(),
        "{}",
        "no rustfmt bin, try running `cargo build` or `cargo build --release` before testing"
    );
    me
}

#[test]
fn verify_check_works() {
    init_log();
    let temp_file = make_temp_file("temp_check.rs");

    Command::new(rustfmt().to_str().unwrap())
        .arg("--check")
        .arg(temp_file.path.to_str().unwrap())
        .status()
        .expect("run with check option failed");
}

#[test]
fn verify_check_works_with_stdin() {
    init_log();

    let mut child = Command::new(rustfmt().to_str().unwrap())
        .arg("--check")
        .stdin(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("run with check option failed");

    {
        let stdin = child.stdin.as_mut().expect("Failed to open stdin");
        stdin
            .write_all("fn main() {}\n".as_bytes())
            .expect("Failed to write to rustfmt --check");
    }
    let output = child
        .wait_with_output()
        .expect("Failed to wait on rustfmt child");
    assert!(output.status.success());
}

#[test]
fn verify_check_l_works_with_stdin() {
    init_log();

    let mut child = Command::new(rustfmt().to_str().unwrap())
        .arg("--check")
        .arg("-l")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("run with check option failed");

    {
        let stdin = child.stdin.as_mut().expect("Failed to open stdin");
        stdin
            .write_all("fn main()\n{}\n".as_bytes())
            .expect("Failed to write to rustfmt --check");
    }
    let output = child
        .wait_with_output()
        .expect("Failed to wait on rustfmt child");
    assert!(output.status.success());
    assert_eq!(std::str::from_utf8(&output.stdout).unwrap(), "<stdin>\n");
}
