use std::path::{Path, PathBuf};

use assert_cmd::Command;
use lopdf::Document;
use predicates::prelude::*;
use tempfile::tempdir;

fn fixture(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join(name)
}

fn pdq() -> Command {
    Command::cargo_bin("pdq").unwrap()
}

fn assert_page_count(path: &Path, expected: usize) {
    let document = Document::load(path)
        .unwrap_or_else(|err| panic!("failed to load {}: {err}", path.display()));
    assert_eq!(
        document.get_pages().len(),
        expected,
        "unexpected page count for {}",
        path.display()
    );
}

fn assert_not_encrypted(path: &Path) {
    let document = Document::load(path)
        .unwrap_or_else(|err| panic!("failed to load {}: {err}", path.display()));
    assert!(
        !document.is_encrypted() && !document.was_encrypted(),
        "{} should be written unencrypted",
        path.display()
    );
}

#[test]
fn split_cli_writes_each_requested_range() {
    let temp = tempdir().unwrap();
    let first = temp.path().join("first.pdf");
    let rest = temp.path().join("rest.pdf");

    pdq()
        .arg("split")
        .arg(fixture("11-pages.pdf"))
        .arg("--out")
        .arg("1-3")
        .arg(&first)
        .arg("--out")
        .arg("4-z")
        .arg(&rest)
        .assert()
        .success();

    assert_page_count(&first, 3);
    assert_page_count(&rest, 8);
}

#[test]
fn split_cli_rejects_invalid_range_syntax() {
    let temp = tempdir().unwrap();
    let output = temp.path().join("out.pdf");

    pdq()
        .arg("split")
        .arg(fixture("11-pages.pdf"))
        .arg("--out")
        .arg("abc")
        .arg(&output)
        .assert()
        .failure()
        .stderr(predicate::str::contains("invalid page number"));

    assert!(!output.exists());
}

#[test]
fn split_cli_rejects_out_of_bounds_range_without_writing() {
    let temp = tempdir().unwrap();
    let output = temp.path().join("out.pdf");

    pdq()
        .arg("split")
        .arg(fixture("11-pages.pdf"))
        .arg("--out")
        .arg("99")
        .arg(&output)
        .assert()
        .failure()
        .stderr(predicate::str::contains("out of bounds"));

    assert!(!output.exists());
}

#[test]
fn split_cli_fails_on_missing_input() {
    let temp = tempdir().unwrap();
    let output = temp.path().join("out.pdf");

    pdq()
        .arg("split")
        .arg(temp.path().join("does-not-exist.pdf"))
        .arg("--out")
        .arg("1")
        .arg(&output)
        .assert()
        .failure();

    assert!(!output.exists());
}

#[test]
fn split_cli_requires_password_for_user_password_input() {
    let temp = tempdir().unwrap();
    let output = temp.path().join("out.pdf");

    pdq()
        .arg("split")
        .arg(fixture("user-password.pdf"))
        .arg("--out")
        .arg("1")
        .arg(&output)
        .assert()
        .failure()
        .stderr(predicate::str::contains("--password"));

    assert!(!output.exists());
}

#[test]
fn split_cli_rejects_wrong_password() {
    let temp = tempdir().unwrap();
    let output = temp.path().join("out.pdf");

    pdq()
        .arg("split")
        .arg(fixture("user-password.pdf"))
        .arg("--password")
        .arg("wrong")
        .arg("--out")
        .arg("1")
        .arg(&output)
        .assert()
        .failure()
        .stderr(predicate::str::contains("invalid password"));

    assert!(!output.exists());
}

#[test]
fn split_cli_decrypts_user_password_input_with_password() {
    let temp = tempdir().unwrap();
    let output = temp.path().join("decrypted.pdf");

    pdq()
        .arg("split")
        .arg(fixture("user-password.pdf"))
        .arg("--password")
        .arg("user")
        .arg("--out")
        .arg("1-3")
        .arg(&output)
        .assert()
        .success();

    assert_page_count(&output, 3);
    assert_not_encrypted(&output);
}

#[test]
fn split_pages_cli_decrypts_owner_only_input_without_password() {
    let temp = tempdir().unwrap();

    pdq()
        .arg("split-pages")
        .arg(fixture("owner-only.pdf"))
        .arg("--output")
        .arg(temp.path().join("page-%d.pdf"))
        .assert()
        .success();

    for page in 1..=11 {
        let path = temp.path().join(format!("page-{page:02}.pdf"));
        assert_page_count(&path, 1);
        assert_not_encrypted(&path);
    }
}

#[test]
fn merge_cli_decrypts_single_encrypted_input() {
    let temp = tempdir().unwrap();
    let merged = temp.path().join("merged.pdf");

    pdq()
        .arg("merge")
        .arg("--output")
        .arg(&merged)
        .arg(fixture("owner-only.pdf"))
        .assert()
        .success();

    assert_page_count(&merged, 11);
    assert_not_encrypted(&merged);
}

#[test]
fn page_count_cli_reads_owner_only_encrypted_input_without_password() {
    pdq()
        .arg("page-count")
        .arg(fixture("owner-only.pdf"))
        .assert()
        .success()
        .stdout(predicate::str::diff("11\n"));
}

#[test]
fn page_count_cli_reads_user_password_input_with_password() {
    pdq()
        .arg("page-count")
        .arg(fixture("user-password.pdf"))
        .arg("--password")
        .arg("user")
        .assert()
        .success()
        .stdout(predicate::str::diff("11\n"));
}

#[test]
fn split_cli_requires_range_and_path_pair_for_out() {
    pdq()
        .arg("split")
        .arg(fixture("11-pages.pdf"))
        .arg("--out")
        .arg("1-3")
        .assert()
        .failure()
        .stderr(predicate::str::contains("--out"));
}

#[test]
fn merge_cli_concatenates_inputs_in_order() {
    let temp = tempdir().unwrap();
    let merged = temp.path().join("merged.pdf");

    pdq()
        .arg("merge")
        .arg("--output")
        .arg(&merged)
        .arg(fixture("11-pages.pdf"))
        .arg(fixture("11-pages-objstm.pdf"))
        .assert()
        .success();

    assert_page_count(&merged, 22);
}

#[test]
fn merge_cli_fails_on_missing_input_without_writing() {
    let temp = tempdir().unwrap();
    let merged = temp.path().join("merged.pdf");

    pdq()
        .arg("merge")
        .arg("--output")
        .arg(&merged)
        .arg(fixture("11-pages.pdf"))
        .arg(temp.path().join("does-not-exist.pdf"))
        .assert()
        .failure();

    assert!(!merged.exists());
}

#[test]
fn merge_cli_requires_at_least_one_input() {
    let temp = tempdir().unwrap();

    pdq()
        .arg("merge")
        .arg("--output")
        .arg(temp.path().join("merged.pdf"))
        .assert()
        .failure()
        .stderr(predicate::str::contains("Usage"));
}

#[test]
fn split_pages_cli_rejects_pattern_without_placeholder() {
    let temp = tempdir().unwrap();

    pdq()
        .arg("split-pages")
        .arg(fixture("11-pages.pdf"))
        .arg("--output")
        .arg(temp.path().join("page.pdf"))
        .assert()
        .failure()
        .stderr(predicate::str::contains("%d"));
}

#[test]
fn split_pages_cli_rejects_pattern_with_multiple_placeholders() {
    let temp = tempdir().unwrap();

    pdq()
        .arg("split-pages")
        .arg(fixture("11-pages.pdf"))
        .arg("--output")
        .arg(temp.path().join("page-%d-%d.pdf"))
        .assert()
        .failure()
        .stderr(predicate::str::contains("%d"));
}

#[test]
fn page_count_cli_strict_flag_reports_total_pages() {
    for input in ["11-pages.pdf", "11-pages-objstm.pdf"] {
        pdq()
            .arg("page-count")
            .arg("--strict")
            .arg(fixture(input))
            .assert()
            .success()
            .stdout(predicate::str::diff("11\n"));
        // Default (trusted /Count) mode must agree with --strict.
        pdq()
            .arg("page-count")
            .arg(fixture(input))
            .assert()
            .success()
            .stdout(predicate::str::diff("11\n"));
    }
}

#[test]
fn page_count_cli_encrypted_inputs_behave_the_same_in_both_modes() {
    for strict in [false, true] {
        // A real user password is required: without it both modes must fail
        // with the password guidance, never a wrong count.
        let mut cmd = pdq();
        cmd.arg("page-count");
        if strict {
            cmd.arg("--strict");
        }
        cmd.arg(fixture("user-password.pdf"))
            .assert()
            .failure()
            .stderr(predicate::str::contains("password"));

        // Owner-password-only files decrypt with the empty user password and
        // must count identically in fast and strict modes.
        let mut cmd = pdq();
        cmd.arg("page-count");
        if strict {
            cmd.arg("--strict");
        }
        cmd.arg(fixture("owner-only.pdf"))
            .assert()
            .success()
            .stdout(predicate::str::diff("11\n"));
    }
}

#[test]
fn split_pages_cli_chunks_pages_per_file() {
    let temp = tempdir().unwrap();

    pdq()
        .arg("split-pages")
        .arg(fixture("11-pages.pdf"))
        .arg("--output")
        .arg(temp.path().join("chunk-%d.pdf"))
        .arg("--pages-per-file")
        .arg("4")
        .assert()
        .success();

    assert_page_count(&temp.path().join("chunk-1.pdf"), 4);
    assert_page_count(&temp.path().join("chunk-2.pdf"), 4);
    assert_page_count(&temp.path().join("chunk-3.pdf"), 3);
    assert!(!temp.path().join("chunk-4.pdf").exists());
}

#[test]
fn split_pages_cli_chunks_and_decrypts_with_password() {
    let temp = tempdir().unwrap();

    pdq()
        .arg("split-pages")
        .arg(fixture("user-password.pdf"))
        .arg("--output")
        .arg(temp.path().join("chunk-%d.pdf"))
        .arg("--pages-per-file")
        .arg("3")
        .arg("--password")
        .arg("user")
        .assert()
        .success();

    for (chunk, expected_pages) in [(1, 3), (2, 3), (3, 3), (4, 2)] {
        let path = temp.path().join(format!("chunk-{chunk}.pdf"));
        assert_page_count(&path, expected_pages);
        assert_not_encrypted(&path);
    }
    assert!(!temp.path().join("chunk-5.pdf").exists());
}

#[test]
fn split_pages_cli_rejects_zero_pages_per_file() {
    let temp = tempdir().unwrap();

    pdq()
        .arg("split-pages")
        .arg(fixture("11-pages.pdf"))
        .arg("--output")
        .arg(temp.path().join("chunk-%d.pdf"))
        .arg("--pages-per-file")
        .arg("0")
        .assert()
        .failure()
        .stderr(predicate::str::contains("pages-per-file"));

    assert!(!temp.path().join("chunk-1.pdf").exists());
}

#[test]
fn cli_without_subcommand_prints_usage() {
    pdq()
        .assert()
        .failure()
        .stderr(predicate::str::contains("Usage"));
}

#[test]
fn cli_help_lists_subcommands() {
    pdq().arg("--help").assert().success().stdout(
        predicate::str::contains("split")
            .and(predicate::str::contains("split-pages"))
            .and(predicate::str::contains("merge")),
    );
}
