use super::{
    BARE_CONFIG_VALUE, Cli, ExitCode, FileConfig, Mode, Options, apply_config,
    apply_gitignore_patterns_from, normalize_reporters, parse_format_mappings, parse_js_number,
    parse_js_usize, parse_size, resolve_config_ignore, resolve_node_exit_code, store_warning,
};
use clap::{CommandFactory, Parser};

#[test]
fn parses_size_suffixes() {
    assert_eq!(parse_size("1b").unwrap(), 1);
    assert_eq!(parse_size("100kb").unwrap(), 102400);
    assert_eq!(parse_size("100KB").unwrap(), 102400);
    assert_eq!(parse_size("2mb").unwrap(), 2 * 1024 * 1024);
    assert_eq!(parse_size("1024").unwrap(), 1024);
    assert_eq!(parse_size("1.5kb").unwrap(), 1536);
    assert_eq!(parse_size("1.5 kb").unwrap(), 1536);
    assert_eq!(parse_size("1.1kb").unwrap(), 1126);
    assert_eq!(parse_size("+1kb").unwrap(), 1024);
    assert_eq!(parse_size("1tb").unwrap(), 1024_u64.pow(4));
    assert_eq!(
        parse_size("1.5tb").unwrap(),
        1024_u64.pow(4) + 1024_u64.pow(4) / 2
    );
    assert_eq!(parse_size("1pb").unwrap(), 1024_u64.pow(5));
    assert_eq!(parse_size("1k").unwrap(), 1);
    assert_eq!(parse_size("1m").unwrap(), 1);
    assert_eq!(parse_size("1 kb extra").unwrap(), 1);
    assert_eq!(parse_size(".5kb").unwrap(), 0);
    assert_eq!(parse_size("nope").unwrap(), 0);
    assert_eq!(parse_size("-1mb").unwrap(), 0);
}

#[test]
fn parses_cli_integer_flags_like_upstream_parse_int() {
    assert_eq!(parse_js_usize("1.5").unwrap(), 1);
    assert_eq!(parse_js_usize("20.9tokens").unwrap(), 20);
    assert_eq!(parse_js_usize("+1000.9").unwrap(), 1000);
    assert_eq!(parse_js_usize("0x10").unwrap(), 16);
    assert!(parse_js_usize(".5").is_err());
    assert!(parse_js_usize("nope").is_err());
    assert!(parse_js_usize("-1").is_err());
}

#[test]
fn parses_threshold_like_upstream_number() {
    assert_eq!(parse_js_number("7").unwrap(), 7.0);
    assert_eq!(parse_js_number("7.5").unwrap(), 7.5);
    assert_eq!(parse_js_number("0x10").unwrap(), 16.0);
    assert_eq!(parse_js_number("0b10").unwrap(), 2.0);
    assert_eq!(parse_js_number("").unwrap(), 0.0);
    assert!(parse_js_number("nope").unwrap().is_nan());
    assert!(parse_js_number("true").unwrap().is_nan());
}

#[test]
fn parses_exit_code_values_like_node_process_exit() {
    assert_eq!(
        resolve_node_exit_code(&ExitCode::String("7".to_string())).unwrap(),
        7
    );
    assert_eq!(
        resolve_node_exit_code(&ExitCode::String("007".to_string())).unwrap(),
        7
    );
    assert_eq!(
        resolve_node_exit_code(&ExitCode::String("0x10".to_string())).unwrap(),
        16
    );
    assert_eq!(
        resolve_node_exit_code(&ExitCode::String("0b10".to_string())).unwrap(),
        2
    );
    assert_eq!(
        resolve_node_exit_code(&ExitCode::String("-1".to_string())).unwrap(),
        -1
    );
    assert_eq!(
        resolve_node_exit_code(&ExitCode::Boolean(false)).unwrap(),
        0
    );
    assert_eq!(
        resolve_node_exit_code(&ExitCode::String("7.5".to_string())).unwrap_err(),
        "RangeError [ERR_OUT_OF_RANGE]: The value of \"code\" is out of range. It must be an integer. Received 7.5"
    );
    assert_eq!(
        resolve_node_exit_code(&ExitCode::String("nope".to_string())).unwrap_err(),
        "TypeError [ERR_INVALID_ARG_TYPE]: The \"code\" argument must be of type number. Received type string ('nope')"
    );
    assert_eq!(
        resolve_node_exit_code(&ExitCode::Boolean(true)).unwrap_err(),
        "TypeError [ERR_INVALID_ARG_TYPE]: The \"code\" argument must be of type number. Received type boolean (true)"
    );
}

#[test]
fn accepts_missing_cli_integer_values_like_upstream() {
    let cli = Cli::parse_from([
        "jscpd-rs",
        ".",
        "--min-lines",
        "--min-tokens",
        "--max-lines",
    ]);
    let options = Options::from_cli(cli).unwrap();

    assert_eq!(options.min_lines, 0);
    assert_eq!(options.min_tokens, 50);
    assert_eq!(options.max_lines, usize::MAX);
}

#[test]
fn accepts_bare_optional_cli_values_that_upstream_continues_with() {
    let cli = Cli::parse_from([
        "jscpd-rs",
        ".",
        "--threshold",
        "--max-size",
        "--output",
        "--pattern",
        "--store",
        "--store-path",
        "--exitCode",
    ]);
    let options = Options::from_cli(cli).unwrap();

    assert_eq!(options.threshold, Some(1.0));
    assert_eq!(options.max_size_bytes, 0);
    assert_eq!(options.output, std::path::PathBuf::from("true"));
    assert!(options.output_is_bare);
    assert_eq!(options.pattern, "true");
    assert_eq!(options.store.as_deref(), Some("true"));
    assert_eq!(
        options.store_path.as_deref(),
        Some(std::path::Path::new("true"))
    );
    assert_eq!(options.exit_code, ExitCode::Boolean(true));
}

#[test]
fn accepts_bare_config_during_cli_parse_then_matches_upstream_runtime_error() {
    let cli = Cli::parse_from(["jscpd-rs", "--config"]);
    assert_eq!(
        cli.config.as_deref(),
        Some(std::path::Path::new(BARE_CONFIG_VALUE))
    );

    let error = Options::from_cli(cli).unwrap_err();
    assert_eq!(
        error.to_string(),
        "TypeError [ERR_INVALID_ARG_TYPE]: The \"paths[0]\" argument must be of type string. Received type boolean (true)"
    );
}

#[test]
fn bare_optional_string_flags_match_upstream_runtime_errors() {
    for (flag, expected) in [
        ("--ignore", "TypeError: cli.ignore.split is not a function"),
        (
            "--ignore-pattern",
            "TypeError: cli.ignorePattern.split is not a function",
        ),
        (
            "--reporters",
            "TypeError: cli.reporters.split is not a function",
        ),
        ("--mode", "TypeError: mode is not a function"),
        ("--format", "TypeError: cli.format.split is not a function"),
        (
            "--formats-exts",
            "TypeError: extensions.split is not a function",
        ),
        (
            "--formats-names",
            "TypeError: extensions.split is not a function",
        ),
    ] {
        let cli = Cli::parse_from(["jscpd-rs", ".", flag]);
        let error = Options::from_cli(cli).unwrap_err();
        assert_eq!(error.to_string(), expected, "{flag}");
    }
}

#[test]
fn repeated_value_flags_match_upstream_last_value_wins_behavior() {
    let cli = Cli::parse_from([
        "jscpd-rs",
        ".",
        "--ignore",
        "first/**",
        "--ignore",
        "second/**",
        "--reporters",
        "json",
        "--reporters",
        "silent",
    ]);
    let options = Options::from_cli(cli).unwrap();

    assert!(options.ignore.contains(&"second/**".to_string()));
    assert!(!options.ignore.contains(&"first/**".to_string()));
    assert_eq!(options.reporters, vec!["silent"]);
}

#[test]
fn malformed_cli_format_mappings_match_upstream_runtime_error() {
    for flag in ["--formats-exts", "--formats-names"] {
        let cli = Cli::parse_from(["jscpd-rs", ".", flag, "javascript"]);
        let error = Options::from_cli(cli).unwrap_err();

        assert_eq!(
            error.to_string(),
            "TypeError: Cannot read properties of undefined (reading 'split')",
            "{flag}"
        );
    }
}

#[test]
fn help_output_keeps_upstream_cli_contract_text() {
    let mut command = Cli::command();
    let mut output = Vec::new();
    command.write_long_help(&mut output).unwrap();
    let help = String::from_utf8(output).unwrap();

    assert!(help.contains("detector of copy/paste in files"));
    assert!(help.contains("Usage: jscpd [options] <path ...>"));
    assert!(help.contains("min size of duplication in code lines (Default is 5)"));
    assert!(help.contains("reporters or list of reporters separated with comma"));
    assert!(help.contains("ignore comments during detection (alias for --mode weak)"));
    assert!(help.contains("output the version number"));
    assert!(!help.contains("[possible values: strict, mild, weak]"));
}

#[test]
fn parses_version_flag_for_upstream_output_shape() {
    let cli = Cli::parse_from(["jscpd-rs", "--version"]);

    assert!(cli.version);
}

#[test]
fn parses_format_mappings() {
    let mappings = parse_format_mappings("javascript:js,ts;python:py");
    assert_eq!(mappings.find_format_for_value("ts"), Some("javascript"));
    assert_eq!(mappings.find_format_for_value("py"), Some("python"));
    assert_eq!(mappings.find_format_for_value("rs"), None);
}

#[test]
fn preserves_cli_format_order_for_debug_output_like_upstream() {
    let cli = Cli::parse_from(["jscpd-rs", "--format", "typescript,javascript", "."]);
    let options = Options::from_cli(cli).unwrap();

    assert_eq!(
        options.format_order.as_deref(),
        Some(["typescript".to_string(), "javascript".to_string()].as_slice())
    );
    assert!(options.formats.as_ref().unwrap().contains("typescript"));
    assert!(options.formats.as_ref().unwrap().contains("javascript"));
}

#[test]
fn appends_cwd_gitignore_patterns_to_debug_option_surface_like_upstream() {
    let nonce = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let dir = std::env::temp_dir().join(format!(
        "jscpd-rs-cli-cwd-gitignore-{}-{nonce}",
        std::process::id()
    ));
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join(".gitignore"), "/target/\nreport\n").unwrap();

    let mut options = Options::default();
    apply_gitignore_patterns_from(&mut options, &dir);

    assert_eq!(
        options.ignore,
        vec![
            "target".to_string(),
            "target/**".to_string(),
            "**/report".to_string(),
            "**/report/**".to_string()
        ]
    );

    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn config_format_mappings_preserve_upstream_object_order() {
    let config: FileConfig = serde_json::from_str(
        r#"{
            "formatsExts": {
                "first": ["dup"],
                "second": ["dup"]
            },
            "formatsNames": {
                "name-first": ["Samefile"],
                "name-second": ["Samefile"]
            }
        }"#,
    )
    .unwrap();
    let mut options = Options::default();

    apply_config(&mut options, config, std::path::Path::new(".")).unwrap();

    assert_eq!(
        options.formats_exts.find_format_for_value("dup"),
        Some("first")
    );
    assert_eq!(
        options.formats_names.find_format_for_value("Samefile"),
        Some("name-first")
    );
}

#[test]
fn config_format_order_is_preserved_for_debug_output_like_upstream() {
    let config: FileConfig =
        serde_json::from_str(r#"{ "format": "typescript,javascript" }"#).unwrap();
    let mut options = Options::default();

    apply_config(&mut options, config, std::path::Path::new(".")).unwrap();

    assert_eq!(
        options.format_order.as_deref(),
        Some(["typescript".to_string(), "javascript".to_string()].as_slice())
    );
}

#[test]
fn config_accepts_string_numbers_that_upstream_coerces() {
    let config: FileConfig = serde_json::from_str(
        r#"{
            "minLines": "0x3",
            "maxLines": "1000",
            "threshold": "0x10"
        }"#,
    )
    .unwrap();
    let mut options = Options::default();

    apply_config(&mut options, config, std::path::Path::new(".")).unwrap();

    assert_eq!(options.min_lines, 3);
    assert_eq!(options.max_lines, 1000);
    assert_eq!(options.threshold, Some(16.0));
}

#[test]
fn default_execution_id_matches_upstream_shape() {
    let options = Options::default();
    let execution_id = options.execution_id.as_deref().unwrap();

    assert!(execution_id.ends_with('Z'));
    assert!(
        regex::Regex::new(r"^\d{4}-\d{2}-\d{2}T")
            .unwrap()
            .is_match(execution_id)
    );
}

#[test]
fn default_path_matches_upstream_cwd() {
    let options = Options::default();

    assert_eq!(options.paths, vec![std::env::current_dir().unwrap()]);
}

#[test]
fn normalizes_silent_reporter_like_upstream() {
    let mut options = Options {
        silent: true,
        reporters: vec!["console".to_string(), "json".to_string()],
        ..Options::default()
    };

    normalize_reporters(&mut options);

    assert_eq!(options.reporters, vec!["json", "silent"]);

    let mut duplicate = Options {
        silent: true,
        reporters: vec!["silent".to_string()],
        ..Options::default()
    };
    normalize_reporters(&mut duplicate);
    assert_eq!(duplicate.reporters, vec!["silent", "silent"]);
}

#[test]
fn normalizes_threshold_reporter_like_upstream() {
    let mut options = Options {
        threshold: Some(10.0),
        reporters: vec!["json".to_string()],
        ..Options::default()
    };

    normalize_reporters(&mut options);

    assert_eq!(options.reporters, vec!["json", "threshold"]);

    let mut duplicate = Options {
        threshold: Some(10.0),
        reporters: vec!["threshold".to_string()],
        ..Options::default()
    };
    normalize_reporters(&mut duplicate);
    assert_eq!(duplicate.reporters, vec!["threshold", "threshold"]);
}

#[test]
fn parses_upstream_workflow_options() {
    let cli = Cli::parse_from([
        "jscpd-rs",
        "--blame",
        "--store",
        "leveldb",
        "--store-path",
        ".jscpd-cache",
        "--noTips",
        ".",
    ]);
    let options = Options::from_cli(cli).unwrap();

    assert!(options.blame);
    assert_eq!(options.store.as_deref(), Some("leveldb"));
    assert_eq!(
        options.store_path.as_deref(),
        Some(std::path::Path::new(".jscpd-cache"))
    );
    assert!(options.no_tips);

    let config: FileConfig = serde_json::from_str(
        r#"{
            "executionId": "run-1",
            "store": "leveldb",
            "storePath": "cache",
            "blame": true,
            "cache": false,
            "noTips": true,
            "listeners": ["console"],
            "tokensToSkip": ["comment", "block-comment"],
            "exitCode": "0x10",
            "reportersOptions": {
                "badge": {
                    "subject": "Duplication"
                }
            }
        }"#,
    )
    .unwrap();
    let mut options = Options::default();
    apply_config(&mut options, config, std::path::Path::new(".")).unwrap();

    assert_eq!(options.execution_id.as_deref(), Some("run-1"));
    assert_eq!(options.store.as_deref(), Some("leveldb"));
    assert_eq!(
        options.store_path.as_deref(),
        Some(std::path::Path::new("cache"))
    );
    assert!(options.blame);
    assert!(!options.cache);
    assert!(options.no_tips);
    assert_eq!(options.exit_code, ExitCode::String("0x10".to_string()));
    assert_eq!(options.listeners, vec!["console"]);
    assert_eq!(options.tokens_to_skip, vec!["comment", "block-comment"]);
    assert_eq!(
        options.reporters_options["badge"]["subject"].as_str(),
        Some("Duplication")
    );
}

#[test]
fn store_warning_matches_upstream_missing_store_fallback() {
    let options = Options {
        store: Some("leveldb".to_string()),
        ..Options::default()
    };

    assert_eq!(
        store_warning(&options).as_deref(),
        Some("store name leveldb not installed.")
    );

    assert!(store_warning(&Options::default()).is_none());
}

#[test]
fn resolves_config_ignore_relative_to_config_dir() {
    let cwd = std::env::current_dir().unwrap();
    let config_dir = cwd.join("configs").join("nested");

    assert_eq!(
        resolve_config_ignore(&config_dir, "dist/**".to_string()).unwrap(),
        "configs/nested/dist/**"
    );
    assert_eq!(
        resolve_config_ignore(&config_dir, "**/generated/**".to_string()).unwrap(),
        "**/generated/**"
    );
}

#[test]
fn config_output_stays_cwd_relative_like_upstream() {
    let config: FileConfig = serde_json::from_str(r#"{ "output": "nested-report" }"#).unwrap();
    let mut options = Options::default();

    apply_config(
        &mut options,
        config,
        std::path::Path::new("/repo/configs/nested"),
    )
    .unwrap();

    assert_eq!(options.output, std::path::PathBuf::from("nested-report"));
}

#[test]
fn skip_comments_does_not_override_explicit_mode() {
    let cli = Cli::parse_from(["jscpd-rs", "--skipComments", "."]);
    let options = Options::from_cli(cli).unwrap();
    assert_eq!(options.mode, Mode::Weak);

    let cli = Cli::parse_from(["jscpd-rs", "--mode", "strict", "--skipComments", "."]);
    let options = Options::from_cli(cli).unwrap();
    assert_eq!(options.mode, Mode::Strict);

    let cli = Cli::parse_from(["jscpd-rs", "--mode", "mild", "--skipComments", "."]);
    let options = Options::from_cli(cli).unwrap();
    assert_eq!(options.mode, Mode::Mild);
}

#[test]
fn invalid_mode_reports_upstream_error_after_cli_parsing() {
    let cli = Cli::parse_from(["jscpd-rs", "--mode", "zzz", "."]);
    let error = Options::from_cli(cli).unwrap_err();

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