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

use anyhow::{Context, Result};
use libpijul::MutTxnT;
use pijul_repository::Repository;
use tempfile::TempDir;

const IDENTITY_TOML: &str = include_str!("fixtures/identity.toml");
const SECRET_KEY_JSON: &str = include_str!("fixtures/secret_key.json");

/// E2E fixture:
/// - owns a temp directory
/// - creates an on-disk Pijul identity under .config/pijul/identities/<name>
/// - initializes a repository with pijul_repository::Repository::init
///
/// This is intended to replace:
/// - ad hoc identity file literals
/// - `pijul init` shelling out
///
/// It does NOT yet record commits/history. For endpoint smoke tests that only
/// need a valid repo and pristine DB, this is enough. If you later need history,
/// add a second layer on top of this fixture.
pub struct RepoFixture {
    temp_dir: TempDir,
    identity_name: String,
}

impl RepoFixture {
    pub fn new() -> Result<Self> {
        let temp_dir = TempDir::new().context("failed to create temp dir")?;
        let fixture = Self {
            temp_dir,
            identity_name: "test".to_string(),
        };

        fixture.prepare_dirs()?;
        fixture.write_identity_files()?;
        fixture.init_repo()?;

        Ok(fixture)
    }

    pub fn repo_root(&self) -> &Path {
        self.temp_dir.path()
    }

    pub fn home_dir(&self) -> &Path {
        self.temp_dir.path()
    }

    pub fn xdg_config_home(&self) -> PathBuf {
        self.temp_dir.path().join(".config")
    }

    pub fn xdg_data_home(&self) -> PathBuf {
        self.temp_dir.path().join(".local").join("share")
    }

    pub fn identity_dir(&self) -> PathBuf {
        self.xdg_config_home()
            .join("pijul")
            .join("identities")
            .join(&self.identity_name)
    }

    pub fn pristine_db_path(&self) -> PathBuf {
        self.repo_root().join(".pijul").join("pristine").join("db")
    }
    /// Environment variables to pass to the server process in e2e tests.
    pub fn apply_env(&self, cmd: &mut std::process::Command) {
        cmd.env("HOME", self.home_dir())
            .env("XDG_CONFIG_HOME", self.xdg_config_home())
            .env("XDG_DATA_HOME", self.xdg_data_home());
    }

    pub fn write_file(&self, rel: &str, contents: &str) -> Result<()> {
        let path = self.repo_root().join(rel);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("failed to create parent dirs for {}", path.display()))?;
        }
        fs::write(&path, contents)
            .with_context(|| format!("failed to write {}", path.display()))?;
        Ok(())
    }

    fn prepare_dirs(&self) -> Result<()> {
        fs::create_dir_all(self.identity_dir())
            .with_context(|| format!("failed to create {}", self.identity_dir().display()))?;

        fs::create_dir_all(self.xdg_data_home().join("pijul")).with_context(|| {
            format!(
                "failed to create {}",
                self.xdg_data_home().join("pijul").display()
            )
        })?;

        Ok(())
    }
    fn write_identity_files(&self) -> Result<()> {
        let identity_toml_path = self.identity_dir().join("identity.toml");
        let secret_key_json_path = self.identity_dir().join("secret_key.json");

        fs::write(&identity_toml_path, IDENTITY_TOML)
            .with_context(|| format!("failed to write {}", identity_toml_path.display()))?;
        fs::write(&secret_key_json_path, SECRET_KEY_JSON)
            .with_context(|| format!("failed to write {}", secret_key_json_path.display()))?;

        Ok(())
    }

    fn init_repo(&self) -> Result<()> {
        let repo = Repository::init(Some(self.repo_root().to_path_buf()), None, None)
            .context("failed to initialize repository")?;

        // `pijul init` creates a `main` channel. The lightweight `pijul_repository`
        // initializer doesn't guarantee that, so ensure one exists for protocol tests.
        let mut txn = repo
            .pristine
            .mut_txn_begin()
            .context("failed to open pristine mut txn")?;
        txn.open_or_create_channel("main")
            .context("failed to create main channel")?;
        txn.commit().context("failed to commit pristine txn")?;

        anyhow::ensure!(
            self.pristine_db_path().exists(),
            "expected pristine db at {}",
            self.pristine_db_path().display()
        );

        Ok(())
    }
}