mod generation;

use std::{io::Write, path::Path};

use crate::{
    generator::generation::MarkdownGeneration, logger::Logger, renderer::RenderOptions,
    util::fence::generate_outer_backticks,
};

const DEFAULT_PROJECT_NAME: &str = "Project Outline";

pub struct MarkdownGenerator<'a> {
    origin_base: &'a Path,
    paths: &'a [&'a Path],
    project_title: Option<&'a str>,
    render_options: RenderOptions,
    root: &'a Path,
}

impl<'a> MarkdownGenerator<'a> {
    pub fn new(paths: &'a [&'a Path]) -> Self {
        MarkdownGenerator {
            origin_base: Path::new("."),
            paths,
            project_title: None,
            render_options: RenderOptions::default(),
            root: Path::new("."),
        }
    }

    pub fn with_root(mut self, root: &'a Path) -> Self {
        self.root = root;
        self
    }

    pub fn with_origin(mut self, origin: &'a Path) -> Self {
        self.origin_base = origin;
        self
    }

    pub fn with_project_title(mut self, project_title: Option<&'a str>) -> Self {
        self.project_title = project_title;
        self
    }

    pub fn with_render_options(mut self, render_options: RenderOptions) -> Self {
        self.render_options = render_options;
        self
    }

    fn get_or_derive_project_title(&self) -> &str {
        if let Some(project_title) = &self.project_title {
            project_title
        } else if let Some(os_str_name) = self.root.file_name()
            && let Some(name) = os_str_name.to_str()
        {
            name
        } else {
            DEFAULT_PROJECT_NAME
        }
    }

    pub fn generate<W: Write, LW: Write>(
        self,
        output: W,
        logger: &Logger<LW>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        MarkdownGeneration::new(self, output, logger)?.run()
    }

    pub fn generate_fenced<W: Write, LW: Write>(
        self,
        mut output: W,
        logger: &Logger<LW>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let mut md_output = Vec::new();
        self.generate(&mut md_output, logger)?;

        let fence = generate_outer_backticks(&md_output);
        writeln!(output, "{}markdown", fence)?;
        output.write_all(&md_output)?;
        writeln!(output, "{}", fence)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::{fs, path::Path};

    use tempfile::tempdir;

    use crate::{
        logger::Verbosity,
        renderer::RenderOptions,
        util::test_support::{logger_with_shared_log, quiet_logger},
    };

    use super::MarkdownGenerator;

    fn generate_to_string(
        generator: MarkdownGenerator,
    ) -> Result<String, Box<dyn std::error::Error>> {
        let mut output = Vec::new();
        let logger = quiet_logger();

        generator.generate(&mut output, &logger)?;

        Ok(String::from_utf8(output).unwrap())
    }

    fn generate_fenced_to_string(
        generator: MarkdownGenerator,
    ) -> Result<String, Box<dyn std::error::Error>> {
        let mut output = Vec::new();
        let logger = quiet_logger();

        generator.generate_fenced(&mut output, &logger)?;

        Ok(String::from_utf8(output).unwrap())
    }

    #[test]
    fn empty_path_list_renders_header_with_specified_project_title() {
        let temp_dir = tempdir().unwrap();
        let paths = [];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"));
        let output = generate_to_string(generator).unwrap();

        assert_eq!(output, "# Project name\n");
    }

    #[test]
    fn project_title_is_derived_from_root_when_not_specified() {
        let temp_dir = tempdir().unwrap();
        let paths = [];
        let root = temp_dir.path().join("repo2markdown");

        let generator = MarkdownGenerator::new(&paths)
            .with_root(&root)
            .with_origin(temp_dir.path())
            .with_project_title(None);
        let output = generate_to_string(generator).unwrap();

        assert_eq!(output, "# repo2markdown\n");
    }

    #[test]
    fn renders_single_file() {
        let temp_dir = tempdir().unwrap();
        fs::write(temp_dir.path().join("main.rs"), "fn main() {}").unwrap();

        let paths = [Path::new("main.rs")];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"));
        let output = generate_to_string(generator).unwrap();

        assert!(output.contains("# Project name\n"));
        assert!(output.contains("## File: main.rs"));
        assert!(output.contains("```rust\nfn main() {}\n```"));
    }

    #[test]
    fn renders_multiple_files_in_input_order() {
        let temp_dir = tempdir().unwrap();

        fs::write(temp_dir.path().join("a.rs"), "A").unwrap();
        fs::write(temp_dir.path().join("b.rs"), "B").unwrap();

        let paths = [Path::new("a.rs"), Path::new("b.rs")];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"));
        let output = generate_to_string(generator).unwrap();

        let a_pos = output.find("## File: a.rs").unwrap();
        let b_pos = output.find("## File: b.rs").unwrap();

        assert!(a_pos < b_pos);
        assert!(output.contains("```rust\nA\n```"));
        assert!(output.contains("```rust\nB\n```"));
    }

    #[test]
    fn resolves_relative_paths_from_origin() {
        let temp_dir = tempdir().unwrap();
        let origin = temp_dir.path().join("src");

        fs::create_dir_all(&origin).unwrap();
        fs::write(origin.join("main.rs"), "fn main() {}").unwrap();

        let paths = [Path::new("main.rs")];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(&origin)
            .with_project_title(Some("Project name"));
        let output = generate_to_string(generator).unwrap();

        assert!(output.contains("## File: src/main.rs"));
        assert!(output.contains("fn main() {}"));
    }

    #[test]
    fn displays_paths_relative_to_root() {
        let temp_dir = tempdir().unwrap();
        let root = temp_dir.path().join("project");
        let origin = root.join("src");

        fs::create_dir_all(&origin).unwrap();
        fs::write(origin.join("main.rs"), "fn main() {}").unwrap();

        let paths = [Path::new("main.rs")];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(&root)
            .with_origin(&origin)
            .with_project_title(Some("Project name"));
        let output = generate_to_string(generator).unwrap();

        assert!(output.contains("## File: src/main.rs"));
        assert!(output.contains("fn main() {}"));
    }

    #[test]
    fn skips_duplicate_paths() {
        let temp_dir = tempdir().unwrap();

        fs::write(temp_dir.path().join("a.rs"), "A").unwrap();

        let paths = [Path::new("a.rs"), Path::new("a.rs")];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"));
        let output = generate_to_string(generator).unwrap();

        assert_eq!(output.matches("## File: a.rs").count(), 1);
    }

    #[test]
    fn skips_lexically_equivalent_duplicate_paths() {
        let temp_dir = tempdir().unwrap();

        fs::create_dir_all(temp_dir.path().join("src")).unwrap();
        fs::write(temp_dir.path().join("a.rs"), "A").unwrap();

        let paths = [Path::new("a.rs"), Path::new("src/../a.rs")];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"));
        let output = generate_to_string(generator).unwrap();

        assert_eq!(output.matches("## File: a.rs").count(), 1);
    }

    #[test]
    fn uses_render_options_for_large_file_placeholders() {
        let temp_dir = tempdir().unwrap();
        fs::write(temp_dir.path().join("big.txt"), "A".repeat(10)).unwrap();
        let paths = [Path::new("big.txt")];

        let render_options = RenderOptions {
            max_file_size: 5,
            placeholder_for_binary_files: false,
            placeholder_for_large_files: true,
        };

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"))
            .with_render_options(render_options);
        let output = generate_to_string(generator).unwrap();

        assert!(output.contains("## File: big.txt"));
        assert!(output.contains("[FILE TOO LARGE]"));
    }

    #[test]
    fn fenced_generation_wraps_empty_markdown_output() {
        let temp_dir = tempdir().unwrap();
        let paths: [&Path; 0] = [];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"));
        let output = generate_fenced_to_string(generator).unwrap();

        assert_eq!(output, "```markdown\n# Project name\n```\n");
    }

    #[test]
    fn fenced_generation_wraps_file_output() {
        let temp_dir = tempdir().unwrap();

        fs::write(temp_dir.path().join("main.rs"), "fn main() {}").unwrap();

        let paths = [Path::new("main.rs")];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"));
        let output = generate_fenced_to_string(generator).unwrap();

        assert!(output.starts_with("````markdown\n"));
        assert!(output.ends_with("\n````\n"));
        assert!(output.contains("# Project name"));
        assert!(output.contains("## File: main.rs"));
        assert!(output.contains("```rust\nfn main() {}\n```"));
    }

    #[test]
    fn fenced_generation_expands_outer_fence_when_inner_markdown_contains_backticks() {
        let temp_dir = tempdir().unwrap();

        fs::write(temp_dir.path().join("backticks.rs"), "````").unwrap();

        let paths = [Path::new("backticks.rs")];

        let generator = MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"));
        let output = generate_fenced_to_string(generator).unwrap();

        assert!(output.starts_with("``````markdown\n"));
        assert!(output.ends_with("\n``````\n"));
        assert!(output.contains("`````rust\n````\n`````"));
    }

    #[test]
    fn normal_verbosity_logs_duplicate_paths() {
        let temp_dir = tempdir().unwrap();
        fs::write(temp_dir.path().join("a.rs"), "A").unwrap();

        let paths = [Path::new("a.rs"), Path::new("a.rs")];

        let mut output = Vec::new();
        let (logger, logs) = logger_with_shared_log(Verbosity::Normal);

        MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"))
            .generate(&mut output, &logger)
            .unwrap();

        assert_eq!(logs.contents(), "Warning: skipping duplicate file: a.rs\n");
    }

    #[test]
    fn quiet_verbosity_suppresses_duplicate_path_warning() {
        let temp_dir = tempdir().unwrap();
        fs::write(temp_dir.path().join("a.rs"), "A").unwrap();

        let paths = [Path::new("a.rs"), Path::new("a.rs")];

        let mut output = Vec::new();
        let (logger, logs) = logger_with_shared_log(Verbosity::Quiet);

        MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"))
            .generate(&mut output, &logger)
            .unwrap();

        assert_eq!(logs.contents(), "");
    }

    #[test]
    fn normal_verbosity_logs_lexically_equivalent_duplicate_paths() {
        let temp_dir = tempdir().unwrap();

        fs::create_dir_all(temp_dir.path().join("src")).unwrap();
        fs::write(temp_dir.path().join("a.rs"), "A").unwrap();

        let paths = [Path::new("a.rs"), Path::new("src/../a.rs")];

        let mut output = Vec::new();
        let (logger, logs) = logger_with_shared_log(Verbosity::Normal);

        MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"))
            .generate(&mut output, &logger)
            .unwrap();

        assert_eq!(logs.contents(), "Warning: skipping duplicate file: a.rs\n");
    }

    #[test]
    fn verbose_verbosity_logs_rendered_files_and_duplicates_in_order() {
        let temp_dir = tempdir().unwrap();

        fs::write(temp_dir.path().join("a.rs"), "A").unwrap();
        fs::write(temp_dir.path().join("b.rs"), "B").unwrap();

        let paths = [Path::new("a.rs"), Path::new("a.rs"), Path::new("b.rs")];

        let mut output = Vec::new();
        let (logger, logs) = logger_with_shared_log(Verbosity::Verbose);

        MarkdownGenerator::new(&paths)
            .with_root(temp_dir.path())
            .with_origin(temp_dir.path())
            .with_project_title(Some("Project name"))
            .generate(&mut output, &logger)
            .unwrap();

        assert_eq!(
            logs.contents(),
            "Info: rendering file: a.rs\n\
            Warning: skipping duplicate file: a.rs\n\
            Info: rendering file: b.rs\n"
        );
    }
}
