use std::path::PathBuf;

use jscpd_rs::{
    Detector, FormatMappings, MemoryStore, Options, SourceFile, Statistic, detect_clones,
    detect_clones_and_statistic, detect_clones_and_statistics, detect_source_files,
    get_default_options, get_format_by_file, get_format_by_file_with_mappings,
    get_options_from_args, get_supported_formats,
};

const DUPLICATE_JS: &str = "const alpha = 1;\nconst beta = 2;\nconst gamma = alpha + beta;\n";

fn fixture_options(path: &str) -> Options {
    Options {
        paths: vec![PathBuf::from(path)],
        reporters: Vec::new(),
        silent: true,
        no_tips: true,
        min_tokens: 20,
        min_lines: 3,
        max_size_bytes: 1024 * 1024,
        ..Options::default()
    }
}

fn in_memory_options() -> Options {
    Options {
        reporters: Vec::new(),
        silent: true,
        no_tips: true,
        min_tokens: 5,
        min_lines: 2,
        ..Options::default()
    }
}

fn javascript_source(source_id: &str, content: &str) -> SourceFile {
    SourceFile {
        source_id: source_id.to_string(),
        format: "javascript".to_string(),
        content: content.to_string(),
    }
}

#[test]
fn public_api_detects_clones_from_paths() {
    let options = fixture_options("jscpd/fixtures/clike/file2.c");

    let clones = detect_clones(&options).expect("detect clones");

    assert_eq!(clones.len(), 1);
    assert_eq!(clones[0].duplication_a.start.line, 18);
    assert_eq!(clones[0].duplication_b.start.line, 8);
}

#[test]
fn public_api_returns_statistics() {
    let options = fixture_options("jscpd/fixtures/clike/file2.c");

    let result = detect_clones_and_statistics(&options).expect("detect with statistics");

    assert_eq!(result.clones.len(), 1);
    assert_eq!(result.statistics.total.clones, 1);
    assert_eq!(result.statistics.total.sources, 1);
}

#[test]
fn public_api_statistic_alias_matches_upstream_name() {
    let options = fixture_options("jscpd/fixtures/clike/file2.c");

    let result = detect_clones_and_statistic(&options).expect("detect with statistic alias");

    assert_eq!(result.clones.len(), 1);
    assert_eq!(result.statistics.total.clones, 1);
}

#[test]
fn public_api_exposes_default_options() {
    let options = get_default_options();

    assert_eq!(options.min_lines, 5);
    assert_eq!(options.min_tokens, 50);
    assert_eq!(options.max_lines, 1000);
    assert_eq!(options.max_size_bytes, 100 * 1024);
    assert_eq!(options.reporters, vec!["console"]);
    assert!(options.cache);
    assert!(options.gitignore);
}

#[test]
fn public_api_parses_options_from_args() {
    let options = get_options_from_args([
        "jscpd",
        "fixtures",
        "--format",
        "javascript,typescript",
        "--reporters",
        "json",
        "--min-tokens",
        "7",
        "--min-lines",
        "2",
        "--max-size",
        "1mb",
        "--noTips",
    ])
    .expect("parse options from argv");

    let expected_formats = vec!["javascript".to_string(), "typescript".to_string()];
    assert_eq!(options.paths, vec![PathBuf::from("fixtures")]);
    assert_eq!(
        options.format_order.as_deref(),
        Some(expected_formats.as_slice())
    );
    assert_eq!(options.reporters, vec!["json"]);
    assert_eq!(options.min_tokens, 7);
    assert_eq!(options.min_lines, 2);
    assert_eq!(options.max_size_bytes, 1024 * 1024);
    assert!(options.no_tips);
}

#[test]
fn public_api_arg_parser_preserves_runtime_option_errors() {
    let error = get_options_from_args(["jscpd", "--mode", "zzz", "."]).unwrap_err();

    assert_eq!(error.to_string(), "Mode zzz does not supported yet.");
}

#[test]
fn public_api_exposes_supported_formats() {
    let formats = get_supported_formats();

    assert_eq!(formats.len(), 223);
    assert!(formats.contains(&"javascript"));
    assert!(formats.contains(&"typescript"));
    assert!(formats.contains(&"rust"));
}

#[test]
fn public_api_resolves_format_by_file() {
    assert_eq!(
        get_format_by_file("src/index.mts").as_deref(),
        Some("typescript")
    );
    assert_eq!(
        get_format_by_file("src/component.vue").as_deref(),
        Some("vue")
    );
}

#[test]
fn public_api_resolves_format_by_custom_mappings() {
    let formats_exts = FormatMappings::from_pairs(vec![("custom", vec!["foo"])]);
    let formats_names = FormatMappings::from_pairs(vec![("makefile", vec!["Buildfile"])]);

    assert_eq!(
        get_format_by_file_with_mappings("demo.foo", &formats_exts, &formats_names).as_deref(),
        Some("custom")
    );
    assert_eq!(
        get_format_by_file_with_mappings("Buildfile", &formats_exts, &formats_names).as_deref(),
        Some("makefile")
    );
    assert_eq!(
        get_format_by_file_with_mappings("src/index.ts", &formats_exts, &formats_names),
        None
    );
}

#[test]
fn public_api_detects_from_in_memory_sources() {
    let files = vec![
        javascript_source("snippet.js", DUPLICATE_JS),
        javascript_source("src/match.js", DUPLICATE_JS),
    ];

    let result = detect_source_files(files, &in_memory_options());

    assert_eq!(result.clones.len(), 1);
    assert_eq!(result.statistics.total.sources, 2);
}

#[test]
fn public_api_exposes_streaming_detector() {
    let mut detector = Detector::new(in_memory_options());

    assert!(
        detector
            .detect("first.js", DUPLICATE_JS, "javascript")
            .is_empty()
    );
    let clones = detector.detect("second.js", DUPLICATE_JS, "javascript");

    assert_eq!(clones.len(), 1);
    assert_eq!(detector.sources().len(), 2);
    assert!(
        clones[0].duplication_a.source_id == "second.js"
            || clones[0].duplication_b.source_id == "second.js"
    );
}

#[test]
fn public_api_detector_clear_and_one_shot_detect_files() {
    let mut detector = Detector::with_sources(
        in_memory_options(),
        vec![javascript_source("seed.js", DUPLICATE_JS)],
    );

    assert_eq!(detector.sources().len(), 1);
    assert_eq!(detector.options().min_tokens, 5);
    detector.options_mut().min_tokens = 4;
    detector.clear();
    assert!(detector.sources().is_empty());

    let result = detector.detect_files(vec![
        javascript_source("first.js", DUPLICATE_JS),
        javascript_source("second.js", DUPLICATE_JS),
    ]);

    assert_eq!(result.clones.len(), 1);
    assert_eq!(result.statistics.total.sources, 2);
}

#[test]
fn public_api_exposes_statistic_collector() {
    let result = detect_source_files(
        vec![
            javascript_source("first.js", DUPLICATE_JS),
            javascript_source("second.js", DUPLICATE_JS),
        ],
        &in_memory_options(),
    );
    let mut statistic = Statistic::new();

    statistic.match_source("first.js", "javascript", 3, 42);
    statistic.match_source("second.js", "javascript", 3, 42);
    statistic.clone_found(&result.clones[0]);

    let stats = statistic.get_statistic();
    assert_eq!(stats.total.sources, 2);
    assert_eq!(stats.total.clones, 1);
    assert_eq!(stats.formats["javascript"].sources["first.js"].sources, 1);
}

#[test]
fn public_api_exposes_memory_store() {
    let mut store = MemoryStore::new();

    assert_eq!(store.current_namespace(), "");
    assert_eq!(
        store.get("missing").unwrap_err().to_string(),
        "key 'missing' not found in namespace ''"
    );
    store.namespace("javascript");
    assert_eq!(store.current_namespace(), "javascript");
    assert_eq!(*store.set("hash", 7usize), 7);
    assert_eq!(*store.get("hash").expect("stored value"), 7);
    store.namespace("typescript");
    let error = store.get("hash").unwrap_err();

    assert_eq!(error.namespace(), "typescript");
    assert_eq!(error.key(), "hash");
    assert_eq!(store.len(), 1);
    store.close();
    assert!(store.is_empty());
}
