//! Shell env writer — `~/.config/llamastash/env.sh`.
//!
//! Drops a sourceable POSIX-sh script with `OPENAI_BASE_URL` /
//! `OPENAI_API_BASE` / `OPENAI_API_KEY` / `LLAMASTASH_API_KEY`. The
//! one-liner the user adds to their rc (`source ~/.config/llamastash/env.sh`)
//! is shell-agnostic at the call site — bash/zsh/dash/sh all
//! source it cleanly.
//!
//! Both `OPENAI_BASE_URL` (current OpenAI SDK name) and
//! `OPENAI_API_BASE` (legacy / Aider) are set so a tool that reads
//! either picks llamastash up. `LLAMASTASH_API_KEY` services the
//! Zed (`<DISPLAY>_API_KEY` convention) and pi.dev
//! (`$LLAMASTASH_API_KEY` config reference) integrations.
//!
//! Mode is `0o644` (not `0o600`) — no secrets are stored; the
//! "api key" is the literal stub `llamastash`. Group/world-read
//! lets multi-user shells pick it up if the user opts in.

use std::path::PathBuf;

use crate::init::external::{Format, PatchContext, ToolPatcher};

pub struct EnvSh;

impl ToolPatcher for EnvSh {
  fn id(&self) -> &'static str {
    "env-sh"
  }
  fn display_name(&self) -> &'static str {
    "Shell env"
  }
  fn default_path(&self) -> Option<PathBuf> {
    crate::util::paths::config_dir().map(|d| d.join("env.sh"))
  }
  fn format(&self) -> Format {
    Format::Raw
  }
  fn unix_mode(&self) -> u32 {
    0o644
  }
  fn build_additions(&self, _ctx: &PatchContext) -> serde_json::Value {
    // Unused for Format::Raw — present to satisfy the trait.
    serde_json::Value::Null
  }
  fn raw_body(&self, ctx: &PatchContext) -> Option<String> {
    let url = &ctx.proxy_base_url;
    let key = &ctx.api_key;
    Some(format!(
      "#!/bin/sh\n\
       # Generated by `llamastash init`. Edits will be overwritten on re-run.\n\
       # Source from your shell rc:\n\
       #   source {}\n\
       \n\
       export OPENAI_BASE_URL=\"{url}\"\n\
       export OPENAI_API_BASE=\"{url}\"\n\
       export OPENAI_API_KEY=\"{key}\"\n\
       export LLAMASTASH_API_KEY=\"{key}\"\n",
      self
        .default_path()
        .map(|p| p.display().to_string())
        .unwrap_or_else(|| "~/.config/llamastash/env.sh".into()),
    ))
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::init::external::{apply, dry_run};

  fn ctx() -> PatchContext {
    PatchContext {
      proxy_base_url: "http://127.0.0.1:11435/v1".into(),
      api_key: "llamastash".into(),
      model_id: None,
      is_embed: false,
    }
  }

  #[test]
  fn writes_shell_script_with_both_env_var_names() {
    let dir = crate::util::test_temp::unique_temp_dir("env-sh-write");
    let path = dir.join("env.sh");
    apply(&EnvSh, &ctx(), Some(path.clone())).expect("apply");
    let body = std::fs::read_to_string(&path).unwrap();
    assert!(body.starts_with("#!/bin/sh"));
    assert!(body.contains("export OPENAI_BASE_URL=\"http://127.0.0.1:11435/v1\""));
    assert!(body.contains("export OPENAI_API_BASE=\"http://127.0.0.1:11435/v1\""));
    assert!(body.contains("export OPENAI_API_KEY=\"llamastash\""));
    assert!(body.contains("export LLAMASTASH_API_KEY=\"llamastash\""));
    std::fs::remove_dir_all(&dir).ok();
  }

  #[cfg(unix)]
  #[test]
  fn unix_mode_is_644_not_600() {
    use std::os::unix::fs::PermissionsExt;
    let dir = crate::util::test_temp::unique_temp_dir("env-sh-mode");
    let path = dir.join("env.sh");
    apply(&EnvSh, &ctx(), Some(path.clone())).expect("apply");
    let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
    assert_eq!(mode, 0o644);
    std::fs::remove_dir_all(&dir).ok();
  }

  #[test]
  fn dry_run_reports_added_when_file_missing() {
    let dir = crate::util::test_temp::unique_temp_dir("env-sh-dry-add");
    let path = dir.join("env.sh");
    let out = dry_run(&EnvSh, &ctx(), Some(path.clone())).expect("dry_run");
    assert_eq!(out.diff_json.len(), 1);
    assert_eq!(out.diff_json[0].kind, "added");
    assert!(!path.exists(), "dry_run never touches disk");
    std::fs::remove_dir_all(&dir).ok();
  }

  #[test]
  fn second_apply_with_same_inputs_produces_empty_diff() {
    let dir = crate::util::test_temp::unique_temp_dir("env-sh-idem");
    let path = dir.join("env.sh");
    apply(&EnvSh, &ctx(), Some(path.clone())).expect("first");
    let second = apply(&EnvSh, &ctx(), Some(path.clone())).expect("second");
    assert!(second.diff_json.is_empty(), "idempotent");
    std::fs::remove_dir_all(&dir).ok();
  }

  #[test]
  fn dry_run_reports_changed_when_url_drifted() {
    let dir = crate::util::test_temp::unique_temp_dir("env-sh-dry-change");
    let path = dir.join("env.sh");
    std::fs::write(
      &path,
      "#!/bin/sh\nexport OPENAI_BASE_URL=\"http://old/v1\"\n",
    )
    .unwrap();
    let out = dry_run(&EnvSh, &ctx(), Some(path)).expect("dry_run");
    assert_eq!(out.diff_json.len(), 1);
    assert_eq!(out.diff_json[0].kind, "changed");
    std::fs::remove_dir_all(&dir).ok();
  }
}
