#![allow(dead_code)]

use std::{
    error::Error,
    fs,
    io::{Cursor, Read},
    path::{Path, PathBuf},
};

use flate2::read::ZlibDecoder;
use pgp::{
    composed::{Deserializable, DetachedSignature, Message, SignedPublicKey, SignedSecretKey},
    types::{KeyDetails as _, Password},
};
use serde::Deserialize;
use sha1::{Digest as _, Sha1};
use sha2::Sha256;
use tempfile::TempDir;

pub type TestResult<T = ()> = Result<T, Box<dyn Error>>;

#[derive(Debug, Deserialize)]
pub struct UpstreamManifest {
    pub format: u8,
    pub project: Vec<UpstreamProject>,
}

#[derive(Debug, Deserialize)]
pub struct UpstreamProject {
    pub name: String,
    pub version: String,
    pub reported_version: String,
    pub tag: String,
    pub commit: String,
    pub source: String,
    pub behavior_source: String,
    pub license: String,
}

#[derive(Debug, Deserialize)]
pub struct BehaviorManifest {
    pub format: u8,
    #[serde(rename = "case")]
    pub cases: Vec<BehaviorCase>,
}

#[derive(Debug, Deserialize)]
pub struct BehaviorCase {
    pub id: String,
    pub area: String,
    pub argv: Vec<String>,
    pub stdin: String,
    pub tty: bool,
    pub status: i32,
    pub outcome: String,
    pub recipients: Vec<String>,
    pub commit: String,
    pub random_stream: Option<String>,
    pub clock: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct GeneratedManifest {
    pub format: u8,
    pub generated_by: String,
    pub fixture_seed: String,
    pub keys: Vec<KeyRecord>,
    pub entries: Vec<EntryRecord>,
    pub repositories: Vec<RepositoryRecord>,
}

#[derive(Debug, Deserialize)]
pub struct KeyRecord {
    pub name: String,
    pub user_id: String,
    pub primary_fingerprint: String,
    pub encryption_subkey_fingerprint: String,
    pub passphrase: String,
    pub public_armor: String,
    pub public_binary: String,
    pub secret_armor: String,
    pub secret_binary: String,
}

#[derive(Debug, Deserialize)]
pub struct EntryRecord {
    pub store: String,
    pub path: String,
    pub plaintext: String,
    pub recipients: Vec<String>,
    pub ciphertext_sha256: String,
}

#[derive(Debug, Deserialize)]
pub struct RepositoryRecord {
    pub name: String,
    pub template: String,
    pub head: String,
    pub commits: Vec<String>,
}

#[derive(Debug, Deserialize)]
pub struct GnuPgAeadFixture {
    pub format: u8,
    pub producer: String,
    pub command_profile: String,
    pub packet_tag: u8,
    pub cipher: String,
    pub aead_mode: String,
    pub recipient_primary_fingerprint: String,
    pub recipient_encryption_subkey_fingerprint: String,
    pub store: String,
    pub entry: String,
    pub plaintext: String,
    pub ciphertext_sha256: String,
}

pub struct FixtureSet {
    root: PathBuf,
    pub upstream: UpstreamManifest,
    pub behavior: BehaviorManifest,
    pub generated: GeneratedManifest,
    pub gnupg_aead: GnuPgAeadFixture,
    pub layouts: toml::Value,
}

impl FixtureSet {
    pub fn load() -> TestResult<Self> {
        let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/compatibility");
        let upstream = parse_toml(root.join("upstream.toml"))?;
        let behavior = parse_toml(root.join("behavior.toml"))?;
        let generated = parse_toml(root.join("generated.toml"))?;
        let gnupg_aead = parse_toml(root.join("gnupg-aead/fixture.toml"))?;
        let layouts = parse_toml(root.join("layouts.toml"))?;
        Ok(Self {
            root,
            upstream,
            behavior,
            generated,
            gnupg_aead,
            layouts,
        })
    }

    pub fn path(&self, relative: impl AsRef<Path>) -> PathBuf {
        self.root.join(relative)
    }

    pub fn read(&self, relative: impl AsRef<Path>) -> TestResult<Vec<u8>> {
        Ok(fs::read(self.path(relative))?)
    }

    pub fn key(&self, name: &str) -> TestResult<&KeyRecord> {
        self.generated
            .keys
            .iter()
            .find(|key| key.name == name)
            .ok_or_else(|| test_error(format!("missing fixture key {name}")))
    }

    pub fn key_by_fingerprint(&self, fingerprint: &str) -> TestResult<&KeyRecord> {
        self.generated
            .keys
            .iter()
            .find(|key| key.primary_fingerprint == fingerprint)
            .ok_or_else(|| test_error(format!("missing fixture key {fingerprint}")))
    }

    pub fn parse_secret_key(&self, key: &KeyRecord) -> TestResult<SignedSecretKey> {
        let bytes = self.read(&key.secret_armor)?;
        let (secret, _) = SignedSecretKey::from_armor_single(Cursor::new(bytes))?;
        Ok(secret)
    }

    pub fn parse_public_key(&self, key: &KeyRecord) -> TestResult<SignedPublicKey> {
        let bytes = self.read(&key.public_armor)?;
        let (public, _) = SignedPublicKey::from_armor_single(Cursor::new(bytes))?;
        Ok(public)
    }

    pub fn materialize_store(&self, name: &str) -> TestResult<TempDir> {
        let temporary = tempfile::tempdir()?;
        copy_tree(&self.path(Path::new("stores").join(name)), temporary.path())?;
        Ok(temporary)
    }

    pub fn materialize_repository(&self, name: &str) -> TestResult<TempDir> {
        let repository = self
            .generated
            .repositories
            .iter()
            .find(|repository| repository.name == name)
            .ok_or_else(|| test_error(format!("missing repository fixture {name}")))?;
        let temporary = tempfile::tempdir()?;
        copy_tree(&self.path(&repository.template), temporary.path())?;
        Ok(temporary)
    }
}

pub fn decrypt_entry(
    fixture: &FixtureSet,
    entry: &EntryRecord,
    key: &KeyRecord,
) -> TestResult<Vec<u8>> {
    decrypt_entry_with_passphrase(fixture, entry, key, &key.passphrase)
}

pub fn decrypt_entry_with_passphrase(
    fixture: &FixtureSet,
    entry: &EntryRecord,
    key: &KeyRecord,
    passphrase: &str,
) -> TestResult<Vec<u8>> {
    let secret = fixture.parse_secret_key(key)?;
    let ciphertext = fixture.read(Path::new("stores").join(&entry.store).join(&entry.path))?;
    let message = Message::from_bytes(Cursor::new(ciphertext))?;
    let mut decrypted = message.decrypt(&Password::from(passphrase), &secret)?;
    Ok(decrypted.as_data_vec()?)
}

pub fn validate_key_record(fixture: &FixtureSet, key: &KeyRecord) -> TestResult {
    let secret = fixture.parse_secret_key(key)?;
    let public = fixture.parse_public_key(key)?;
    secret.verify_bindings()?;
    public.verify_bindings()?;

    let primary = format!("{:X}", secret.primary_key.fingerprint());
    let encryption = format!("{:X}", secret.secret_subkeys[0].fingerprint());
    if primary != key.primary_fingerprint || encryption != key.encryption_subkey_fingerprint {
        return Err(test_error(format!("fingerprint mismatch for {}", key.name)));
    }
    if secret.to_public_key() != public {
        return Err(test_error(format!(
            "public and secret fixture mismatch for {}",
            key.name
        )));
    }

    let binary_secret = fixture.read(&key.secret_binary)?;
    let binary_public = fixture.read(&key.public_binary)?;
    let parsed_secret = SignedSecretKey::from_bytes(Cursor::new(binary_secret))?;
    let parsed_public = SignedPublicKey::from_bytes(Cursor::new(binary_public))?;
    if parsed_secret != secret || parsed_public != public {
        return Err(test_error(format!(
            "armored and binary key mismatch for {}",
            key.name
        )));
    }
    Ok(())
}

pub fn validate_entry_record(fixture: &FixtureSet, entry: &EntryRecord) -> TestResult {
    let ciphertext = fixture.read(Path::new("stores").join(&entry.store).join(&entry.path))?;
    let digest = hex::encode(Sha256::digest(&ciphertext));
    if digest != entry.ciphertext_sha256 {
        return Err(test_error(format!(
            "ciphertext digest mismatch for {}/{}",
            entry.store, entry.path
        )));
    }

    let expected = fixture.read(
        Path::new("expected")
            .join(&entry.store)
            .join(&entry.plaintext),
    )?;
    for recipient in &entry.recipients {
        let key = fixture.key_by_fingerprint(recipient)?;
        let actual = decrypt_entry(fixture, entry, key)?;
        if actual != expected {
            return Err(test_error(format!(
                "plaintext mismatch for {}/{} and recipient {}",
                entry.store, entry.path, key.name
            )));
        }
    }
    Ok(())
}

pub fn validate_recipient_signature(fixture: &FixtureSet, store: &str, signer: &str) -> TestResult {
    let key = fixture.key(signer)?;
    let public = fixture.parse_public_key(key)?;
    let recipient_path = Path::new("stores").join(store).join(".gpg-id");
    let signature_path = Path::new("stores").join(store).join(".gpg-id.sig");
    let contents = fixture.read(recipient_path)?;
    let signature = DetachedSignature::from_bytes(Cursor::new(fixture.read(signature_path)?))?;
    signature.verify(&public.primary_key, &contents)?;
    Ok(())
}

pub fn validate_repository(fixture: &FixtureSet, repository: &RepositoryRecord) -> TestResult {
    let root = fixture.path(&repository.template);
    let head_ref = fs::read_to_string(root.join("HEAD"))?;
    if head_ref != "ref: refs/heads/main\n" {
        return Err(test_error(format!(
            "unexpected HEAD for {}",
            repository.name
        )));
    }
    let head = fs::read_to_string(root.join("refs/heads/main"))?
        .trim()
        .to_owned();
    if head != repository.head {
        return Err(test_error(format!("head mismatch for {}", repository.name)));
    }

    for path in loose_object_paths(&root.join("objects"))? {
        let canonical = inflate(&path)?;
        let actual = hex::encode(Sha1::digest(&canonical));
        let directory = path
            .parent()
            .and_then(Path::file_name)
            .and_then(|name| name.to_str())
            .ok_or_else(|| test_error("invalid loose object directory"))?;
        let file = path
            .file_name()
            .and_then(|name| name.to_str())
            .ok_or_else(|| test_error("invalid loose object name"))?;
        let expected = format!("{directory}{file}");
        if actual != expected {
            return Err(test_error(format!(
                "invalid loose object {expected} in {}",
                repository.name
            )));
        }
    }

    for commit in &repository.commits {
        let canonical = read_git_object(&root, commit)?;
        if !canonical.starts_with(b"commit ") {
            return Err(test_error(format!(
                "{commit} is not a commit in {}",
                repository.name
            )));
        }
        let separator = canonical
            .iter()
            .position(|byte| *byte == 0)
            .ok_or_else(|| test_error("Git object has no header separator"))?;
        let body = std::str::from_utf8(&canonical[separator + 1..])?;
        let tree = body
            .lines()
            .find_map(|line| line.strip_prefix("tree "))
            .ok_or_else(|| test_error("commit has no tree"))?;
        let tree_object = read_git_object(&root, tree)?;
        if !tree_object.starts_with(b"tree ") {
            return Err(test_error("commit tree points to a non-tree object"));
        }
    }
    Ok(())
}

fn parse_toml<T>(path: PathBuf) -> TestResult<T>
where
    T: for<'de> Deserialize<'de>,
{
    Ok(toml::from_str(&fs::read_to_string(path)?)?)
}

fn copy_tree(source: &Path, destination: &Path) -> TestResult {
    if !source.is_dir() {
        return Err(test_error(format!(
            "fixture source is not a directory: {}",
            source.display()
        )));
    }
    for entry in fs::read_dir(source)? {
        let entry = entry?;
        let source_path = entry.path();
        let destination_path = destination.join(entry.file_name());
        let kind = entry.file_type()?;
        if kind.is_dir() {
            fs::create_dir_all(&destination_path)?;
            copy_tree(&source_path, &destination_path)?;
        } else if kind.is_file() {
            fs::copy(source_path, destination_path)?;
        } else {
            return Err(test_error(format!(
                "unsupported checked-in fixture type: {}",
                source_path.display()
            )));
        }
    }
    Ok(())
}

fn loose_object_paths(objects: &Path) -> TestResult<Vec<PathBuf>> {
    let mut paths = Vec::new();
    for directory in fs::read_dir(objects)? {
        let directory = directory?;
        if !directory.file_type()?.is_dir() {
            continue;
        }
        let directory_name = directory.file_name();
        if directory_name.to_string_lossy().len() != 2 {
            continue;
        }
        for object in fs::read_dir(directory.path())? {
            let object = object?;
            if object.file_type()?.is_file() {
                paths.push(object.path());
            }
        }
    }
    paths.sort();
    Ok(paths)
}

fn read_git_object(repository: &Path, id: &str) -> TestResult<Vec<u8>> {
    if id.len() != 40 || !id.bytes().all(|byte| byte.is_ascii_hexdigit()) {
        return Err(test_error(format!("invalid Git object ID {id}")));
    }
    inflate(&repository.join("objects").join(&id[..2]).join(&id[2..]))
}

fn inflate(path: &Path) -> TestResult<Vec<u8>> {
    let mut decoder = ZlibDecoder::new(fs::File::open(path)?);
    let mut canonical = Vec::new();
    decoder.read_to_end(&mut canonical)?;
    Ok(canonical)
}

fn test_error(message: impl Into<String>) -> Box<dyn Error> {
    Box::new(std::io::Error::other(message.into()))
}
