#![forbid(unsafe_code)]

mod support;

use std::collections::{BTreeMap, BTreeSet};

use ironstorage::command::parse_from;

use support::compatibility::{
    FixtureSet, TestResult, decrypt_entry_with_passphrase, validate_entry_record,
    validate_key_record, validate_recipient_signature, validate_repository,
};

#[test]
fn pins_the_selected_upstream_revisions() -> TestResult {
    let fixture = FixtureSet::load()?;
    assert_eq!(fixture.upstream.format, 1);
    assert_eq!(fixture.upstream.project.len(), 2);

    let projects = fixture
        .upstream
        .project
        .iter()
        .map(|project| (project.name.as_str(), project))
        .collect::<BTreeMap<_, _>>();
    let password_store = projects["password-store"];
    assert_eq!(password_store.version, "1.7.4");
    assert_eq!(password_store.reported_version, "1.7.4");
    assert_eq!(
        password_store.commit,
        "1078f2514d579178d5df7042c6a790e9c9b731ad"
    );
    let pass_otp = projects["pass-otp"];
    assert_eq!(pass_otp.version, "1.2.0");
    assert_eq!(pass_otp.reported_version, "1.1.1");
    assert_eq!(pass_otp.commit, "1e9d10ca75ae1a8672a7f192809713463657778e");
    for project in projects.values() {
        assert_eq!(project.commit.len(), 40);
        assert!(project.commit.bytes().all(|byte| byte.is_ascii_hexdigit()));
        assert!(project.source.starts_with("https://"));
        assert!(!project.behavior_source.is_empty());
        assert!(project.license.starts_with("GPL-"));
    }
    Ok(())
}

#[test]
fn behavior_catalog_covers_the_milestone_contract() -> TestResult {
    let fixture = FixtureSet::load()?;
    assert_eq!(fixture.behavior.format, 1);
    assert!(fixture.behavior.cases.len() >= 65);

    let mut ids = BTreeSet::new();
    let mut commands = BTreeSet::new();
    let mut flags = BTreeSet::new();
    let mut areas = BTreeSet::new();
    let mut has_success = false;
    let mut has_failure = false;

    for case in &fixture.behavior.cases {
        assert!(
            ids.insert(case.id.as_str()),
            "duplicate case ID {}",
            case.id
        );
        assert!(
            matches!(case.status, 0 | 1),
            "invalid status in {}",
            case.id
        );
        has_success |= case.status == 0;
        has_failure |= case.status != 0;
        areas.insert(case.area.as_str());
        if let Some(command) = case.argv.first() {
            commands.insert(command.as_str());
        }
        for argument in &case.argv {
            if argument.starts_with('-') {
                let flag = argument.split('=').next().unwrap_or(argument);
                flags.insert(flag);
            }
        }
        if case.status != 0 {
            assert!(case.recipients.is_empty(), "failed case has recipients");
            assert!(case.commit.is_empty(), "failed case has a commit");
        }
        assert!(!case.outcome.is_empty());
        assert!(!case.stdin.is_empty());
    }

    assert!(has_success && has_failure);
    for area in [
        "init", "show", "list", "find", "grep", "insert", "edit", "generate", "remove", "move",
        "copy", "git", "otp", "meta",
    ] {
        assert!(areas.contains(area), "missing behavior area {area}");
    }
    for command in [
        "init",
        "show",
        "ls",
        "list",
        "find",
        "search",
        "grep",
        "insert",
        "add",
        "edit",
        "generate",
        "rm",
        "remove",
        "delete",
        "mv",
        "rename",
        "cp",
        "copy",
        "git",
        "otp",
        "help",
        "version",
        "--help",
        "--version",
    ] {
        assert!(
            commands.contains(command),
            "missing command or alias {command}"
        );
    }
    for flag in [
        "-p",
        "--path",
        "-c",
        "--clip",
        "-q2",
        "--qrcode",
        "-e",
        "--echo",
        "-m",
        "--multiline",
        "-f",
        "--force",
        "-n",
        "--no-symbols",
        "-i",
        "--in-place",
        "-r",
        "--recursive",
        "-s",
        "--secret",
        "-a",
        "--account",
        "--issuer",
    ] {
        assert!(flags.contains(flag), "missing option spelling {flag}");
    }
    Ok(())
}

#[test]
fn every_compatibility_case_reaches_the_typed_command_contract() -> TestResult {
    let fixture = FixtureSet::load()?;
    for case in &fixture.behavior.cases {
        let arguments = std::iter::once("ironstorage").chain(case.argv.iter().map(String::as_str));
        let parsed = parse_from(arguments);
        let parser_rejection = case.outcome.contains("usage-error")
            || case.outcome.contains("unsupported-option")
            || matches!(
                case.id.as_str(),
                "generate-zero" | "git-unsupported-passthrough"
            );
        assert_eq!(
            parsed.is_err(),
            parser_rejection,
            "unexpected command-contract result for {}: {:?}",
            case.id,
            parsed
        );
        if let Err(error) = parsed {
            assert_eq!(
                error.exit_code(),
                case.status as u8,
                "status for {}",
                case.id
            );
        }
    }
    Ok(())
}

#[test]
fn generated_openpgp_fixtures_are_self_consistent() -> TestResult {
    let fixture = FixtureSet::load()?;
    assert_eq!(fixture.generated.format, 1);
    assert_eq!(fixture.generated.keys.len(), 2);
    assert!(fixture.generated.entries.len() >= 8);
    for key in &fixture.generated.keys {
        validate_key_record(&fixture, key)?;
    }
    for entry in &fixture.generated.entries {
        validate_entry_record(&fixture, entry)?;
    }
    let bob_entry = fixture
        .generated
        .entries
        .iter()
        .find(|entry| entry.store == "basic" && entry.path == "team/service.gpg")
        .expect("Bob-only entry");
    let bob = fixture.key("bob")?;
    assert!(
        decrypt_entry_with_passphrase(&fixture, bob_entry, bob, "fixture-alice-passphrase",)
            .is_err(),
        "the encrypted secret-key passphrase must be required"
    );
    validate_recipient_signature(&fixture, "basic", "alice")?;
    validate_recipient_signature(&fixture, "basic/team", "bob")?;
    Ok(())
}

#[test]
fn generated_git_repositories_have_valid_objects_and_refs() -> TestResult {
    let fixture = FixtureSet::load()?;
    assert_eq!(fixture.generated.repositories.len(), 3);
    for repository in &fixture.generated.repositories {
        validate_repository(&fixture, repository)?;
    }
    Ok(())
}

#[test]
fn fixtures_materialize_without_touching_a_user_store() -> TestResult {
    let fixture = FixtureSet::load()?;
    let store = fixture.materialize_store("basic")?;
    assert!(store.path().join(".gpg-id").is_file());
    assert!(store.path().join("email/personal.gpg").is_file());
    assert!(store.path().join("team/.gpg-id").is_file());

    let repository = fixture.materialize_repository("basic")?;
    assert!(repository.path().join("HEAD").is_file());
    assert!(repository.path().join("objects").is_dir());
    assert!(repository.path().join("refs/heads/main").is_file());
    Ok(())
}

#[test]
fn hostile_and_interruption_recipes_are_present() -> TestResult {
    let fixture = FixtureSet::load()?;
    let nodes = fixture
        .layouts
        .get("synthetic_node")
        .and_then(toml::Value::as_array)
        .expect("synthetic_node array");
    assert!(nodes.len() >= 4);
    let outcomes = nodes
        .iter()
        .filter_map(|node| node.get("expected"))
        .filter_map(toml::Value::as_str)
        .collect::<BTreeSet<_>>();
    assert!(outcomes.contains("reject-symlink-escape"));
    assert!(outcomes.contains("reject-unsupported-file-type"));
    assert!(outcomes.contains("require-trailing-slash-disambiguation"));

    let interruptions = fixture
        .layouts
        .get("interruption")
        .and_then(toml::Value::as_array)
        .expect("interruption array");
    assert_eq!(interruptions.len(), 3);
    Ok(())
}
