// Copyright (c) 2017 fd developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.

use std;
use std::env;
use std::fs;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process;

#[cfg(unix)]
use std::os::unix;

#[cfg(windows)]
use std::os::windows;

use tempdir::TempDir;

/// Environment for the integration tests.
pub struct TestEnv {
    /// Temporary working directory.
    temp_dir: TempDir,

    /// Path to the *fd* executable.
    fd_exe: PathBuf,

    /// Normalize each line by sorting the whitespace-separated words
    normalize_line: bool,
}

/// Create the working directory and the test files.
fn create_working_directory(
    directories: &[&'static str],
    files: &[&'static str],
) -> Result<TempDir, io::Error> {
    let temp_dir = TempDir::new("fd-tests")?;

    {
        let root = temp_dir.path();

        // Pretend that this is a Git repository in order for `.gitignore` files to be respected
        fs::create_dir_all(root.join(".git"))?;

        for directory in directories {
            fs::create_dir_all(root.join(directory))?;
        }

        for file in files {
            fs::File::create(root.join(file))?;
        }

        #[cfg(unix)]
        unix::fs::symlink(root.join("one/two"), root.join("symlink"))?;

        // Note: creating symlinks on Windows requires the `SeCreateSymbolicLinkPrivilege` which
        // is by default only granted for administrators.
        #[cfg(windows)]
        windows::fs::symlink_dir(root.join("one/two"), root.join("symlink"))?;

        fs::File::create(root.join(".fdignore"))?.write_all(b"fdignored.foo")?;

        fs::File::create(root.join(".gitignore"))?.write_all(b"gitignored.foo")?;
    }

    Ok(temp_dir)
}

/// Find the *fd* executable.
fn find_fd_exe() -> PathBuf {
    // Tests exe is in target/debug/deps, the *fd* exe is in target/debug
    let root = env::current_exe()
        .expect("tests executable")
        .parent()
        .expect("tests executable directory")
        .parent()
        .expect("fd executable directory")
        .to_path_buf();

    let exe_name = if cfg!(windows) { "fd.exe" } else { "fd" };

    root.join(exe_name)
}

/// Format an error message for when *fd* did not exit successfully.
fn format_exit_error(args: &[&str], output: &process::Output) -> String {
    format!(
        "`fd {}` did not exit successfully.\nstdout:\n---\n{}---\nstderr:\n---\n{}---",
        args.join(" "),
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
}

/// Format an error message for when the output of *fd* did not match the expected output.
fn format_output_error(args: &[&str], expected: &str, actual: &str) -> String {
    // Generate diff text.
    let diff_text = diff::lines(expected, actual)
        .into_iter()
        .map(|diff| match diff {
            diff::Result::Left(l) => format!("-{}", l),
            diff::Result::Both(l, _) => format!(" {}", l),
            diff::Result::Right(r) => format!("+{}", r),
        })
        .collect::<Vec<_>>()
        .join("\n");

    format!(
        concat!(
            "`fd {}` did not produce the expected output.\n",
            "Showing diff between expected and actual:\n{}\n"
        ),
        args.join(" "),
        diff_text
    )
}

/// Normalize the output for comparison.
fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String {
    // Split into lines and normalize separators.
    let mut lines = s
        .replace('\0', "NULL\n")
        .lines()
        .map(|line| {
            let line = if trim_start { line.trim_start() } else { line };
            let line = line.replace('/', &std::path::MAIN_SEPARATOR.to_string());
            if normalize_line {
                let mut words: Vec<_> = line.split_whitespace().collect();
                words.sort();
                return words.join(" ");
            }
            line
        })
        .collect::<Vec<_>>();

    lines.sort();
    lines.join("\n")
}

impl TestEnv {
    pub fn new(directories: &[&'static str], files: &[&'static str]) -> TestEnv {
        let temp_dir = create_working_directory(directories, files).expect("working directory");
        let fd_exe = find_fd_exe();

        TestEnv {
            temp_dir,
            fd_exe,
            normalize_line: false,
        }
    }

    pub fn normalize_line(self, normalize: bool) -> TestEnv {
        TestEnv {
            temp_dir: self.temp_dir,
            fd_exe: self.fd_exe,
            normalize_line: normalize,
        }
    }

    /// Get the root directory for the tests.
    pub fn test_root(&self) -> PathBuf {
        self.temp_dir.path().to_path_buf()
    }

    /// Get the root directory of the file system.
    pub fn system_root(&self) -> PathBuf {
        let mut components = self.temp_dir.path().components();
        PathBuf::from(components.next().expect("root directory").as_os_str())
    }

    /// Assert that calling *fd* with the specified arguments produces the expected output.
    pub fn assert_output(&self, args: &[&str], expected: &str) {
        self.assert_output_subdirectory(".", args, expected)
    }

    /// Assert that calling *fd* in the specified path under the root working directory,
    /// and with the specified arguments produces the expected output.
    pub fn assert_output_subdirectory<P: AsRef<Path>>(
        &self,
        path: P,
        args: &[&str],
        expected: &str,
    ) {
        // Setup *fd* command.
        let mut cmd = process::Command::new(&self.fd_exe);
        cmd.current_dir(self.temp_dir.path().join(path));
        cmd.args(args);

        // Run *fd*.
        let output = cmd.output().expect("fd output");

        // Check for exit status.
        if !output.status.success() {
            panic!(format_exit_error(args, &output));
        }

        // Normalize both expected and actual output.
        let expected = normalize_output(expected, true, self.normalize_line);
        let actual = normalize_output(
            &String::from_utf8_lossy(&output.stdout),
            false,
            self.normalize_line,
        );

        // Compare actual output to expected output.
        if expected != actual {
            panic!(format_output_error(args, &expected, &actual));
        }
    }

    /// Assert that calling *fd* with the specified arguments produces the expected error.
    pub fn assert_error(&self, args: &[&str], expected: &str) {
        self.assert_error_subdirectory(".", args, expected)
    }

    /// Assert that calling *fd* in the specified path under the root working directory,
    /// and with the specified arguments produces an error with the expected message.
    fn assert_error_subdirectory<P: AsRef<Path>>(&self, path: P, args: &[&str], expected: &str) {
        // Setup *fd* command.
        let mut cmd = process::Command::new(&self.fd_exe);
        cmd.current_dir(self.temp_dir.path().join(path));
        cmd.args(args);

        // Run *fd*.
        let output = cmd.output().expect("fd output");

        // Check for exit status.
        if output.status.success() {
            panic!("error '{}' did not occur.", expected);
        }

        // Compare actual output to expected output.
        let actual = String::from_utf8_lossy(&output.stderr);
        if !actual.starts_with(expected) {
            panic!(format_output_error(args, &expected, &actual));
        }
    }
}
