use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

#[derive(Debug, Default, Deserialize)]
pub struct Config {
    /// Rust type → Python type overrides. Keys are Rust **path** spellings (e.g. `PyBbox`, `a::Foo`).
    /// Literal tuple types in source are not paths and cannot be keyed; use a `type` alias and map that name.
    /// Aliases listed here are preserved (not expanded to their definition) so nested types like `Vec<Alias>` work.
    #[serde(default)]
    pub type_map: HashMap<String, String>,

    #[serde(default)]
    pub features: FeaturesConfig,

    #[serde(default)]
    pub fallback: FallbackConfig,

    #[serde(default)]
    pub output: OutputConfig,

    /// Manual stub overrides for specific items
    #[serde(default, rename = "override")]
    pub overrides: Vec<OverrideEntry>,

    /// Per-file `__all__` customisation: include/exclude names and override the private filter.
    #[serde(default, rename = "all")]
    pub all: Vec<AllEntry>,

    /// Extra Python snippets to splice into specific generated `.pyi` files (after generation, before write).
    #[serde(default, rename = "add_content")]
    pub add_content: Vec<AddContentEntry>,

    /// After generating .pyi files, run these format commands with generated .pyi paths appended.
    /// Each entry is a command string, e.g. "ruff format", "black", "ruff check --select I --fix".
    #[serde(default)]
    pub format: Vec<String>,
}

#[derive(Debug, Default, Deserialize)]
pub struct FeaturesConfig {
    /// cfg features considered active during parsing (used to evaluate #[cfg(feature = "...")]).
    #[serde(default)]
    pub enabled: Vec<String>,
}

#[derive(Debug, Deserialize)]
pub struct FallbackConfig {
    /// What to do when a type cannot be inferred statically:
    /// "any" (default) | "error" | "skip"
    #[serde(default = "default_fallback_strategy")]
    pub strategy: FallbackStrategy,
}

impl Default for FallbackConfig {
    fn default() -> Self {
        Self {
            strategy: FallbackStrategy::Any,
        }
    }
}

#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum FallbackStrategy {
    /// Emit `Any` and print a warning (default)
    #[default]
    Any,
    /// Hard error — stop generation and report the unknown type
    Error,
    /// Silently skip items with unresolvable types
    Skip,
}

fn default_fallback_strategy() -> FallbackStrategy {
    FallbackStrategy::Any
}

#[derive(Debug, Deserialize)]
pub struct OutputConfig {
    /// Python version string, affects `t.Optional[T]` vs `T | None` syntax (and related typing output).
    #[serde(default = "default_python_version")]
    pub python_version: String,

    /// Prepend "# Auto-generated by rylai" header
    #[serde(default = "default_true")]
    pub add_header: bool,

    /// Global default: exclude names that start with `_` (private / dunder) from `__all__`.
    /// Can be overridden per file via `[[all]]`.
    #[serde(default)]
    pub all_include_private: bool,
}

impl Default for OutputConfig {
    fn default() -> Self {
        Self {
            python_version: default_python_version(),
            add_header: true,
            all_include_private: false,
        }
    }
}

fn default_python_version() -> String {
    "3.10".to_string()
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, Deserialize)]
pub struct OverrideEntry {
    /// Fully-qualified item path, e.g. "my_module::complex_function"
    pub item: String,
    /// Python header line for a top-level `def` or `class` (single line, usually ending with `:`).
    /// Trailing `...` is optional and stripped; if the Rust item has no `///` doc, Rylai emits `...` as the body.
    /// Mutually exclusive with [`Self::param_types`] and [`Self::return_type`]; validated in [`Config::validate_overrides`].
    #[serde(default)]
    pub stub: Option<String>,
    /// Per-parameter Python type strings (full annotations after `:`) merged into the generated
    /// signature; keys are parameter names (`kwargs` and `**kwargs` normalize to the same key).
    /// Incompatible with [`Self::stub`]; may be combined with [`Self::return_type`].
    #[serde(default)]
    pub param_types: Option<HashMap<String, String>>,
    /// Full Python return type string for the generated `def` line (replaces Rust-inferred return).
    /// Incompatible with [`Self::stub`]; may be combined with [`Self::param_types`].
    #[serde(default)]
    pub return_type: Option<String>,
}

/// Normalize a `param_types` config key so `kwargs`, `*args`, and `**kwargs` spellings map to the same bare name.
pub fn normalize_param_types_key(key: &str) -> String {
    let s = key.trim();
    if let Some(rest) = s.strip_prefix("**") {
        return rest.trim().to_string();
    }
    if let Some(rest) = s.strip_prefix('*') {
        return rest.trim().to_string();
    }
    s.to_string()
}

/// Where to insert [`AddContentEntry::content`] in a generated `.pyi` file.
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum AddContentLocation {
    /// Immediately after the `# Auto-generated by rylai...` banner (if present); otherwise at the start of the file.
    Head,
    /// After the first line `import typing as t`.
    AfterImportTyping,
    /// At end of file.
    Tail,
}

/// Per-file `__all__` customisation rules (from `[[all]]` in rylai.toml).
///
/// `exclude` has the highest priority; `include` overrides the private filter for names that were
/// actually emitted; `include_private` overrides the global [`OutputConfig::all_include_private`]
/// for this file only.
#[derive(Debug, Deserialize)]
pub struct AllEntry {
    /// Relative path using `/`, e.g. `abcd.pyi` or `pkg/sub.pyi` (must match output layout).
    pub file: String,
    /// Override the global `all_include_private` for this file only.
    pub include_private: Option<bool>,
    /// Top-level names already emitted in this stub to keep in `__all__` even when the `_` filter
    /// would drop them (cannot add symbols that were not generated).
    #[serde(default)]
    pub include: Vec<String>,
    /// Names that are always excluded from `__all__` (highest priority).
    #[serde(default)]
    pub exclude: Vec<String>,
}

/// Resolved per-file `__all__` configuration after merging global defaults with all matching
/// `[[all]]` entries for a given stub file.
pub struct ResolvedAllConfig {
    pub include_private: bool,
    pub include: std::collections::HashSet<String>,
    pub exclude: std::collections::HashSet<String>,
}

/// Merge global `[output]` defaults with every `[[all]]` entry whose `file` matches `rel_path`.
///
/// Multiple matching entries are merged: `include`/`exclude` sets are unioned; the **last**
/// matching entry's `include_private` (if set) wins.
pub fn resolve_all_config(config: &Config, rel_path: &Path) -> anyhow::Result<ResolvedAllConfig> {
    let key = canonical_stub_rel_path(rel_path);
    let mut include_private = config.output.all_include_private;
    let mut include = std::collections::HashSet::new();
    let mut exclude = std::collections::HashSet::new();

    for e in &config.all {
        let p = normalize_add_content_file(&e.file)
            .map_err(|err| anyhow::anyhow!("[[all]].file {:?}: {}", e.file, err))?;
        if canonical_stub_rel_path(&p) != key {
            continue;
        }
        if let Some(ip) = e.include_private {
            include_private = ip;
        }
        for name in &e.include {
            include.insert(name.clone());
        }
        for name in &e.exclude {
            exclude.insert(name.clone());
        }
    }

    Ok(ResolvedAllConfig {
        include_private,
        include,
        exclude,
    })
}

/// Inject raw Python into a generated stub file identified by path relative to `-o`.
#[derive(Debug, Deserialize)]
pub struct AddContentEntry {
    /// Relative path using `/`, e.g. `abcd.pyi` or `pkg/sub.pyi` (must match output layout).
    pub file: String,
    pub location: AddContentLocation,
    /// Raw Python. A trailing newline is appended automatically when missing so you need not end with `\n`.
    pub content: String,
}

/// Normalize `file` from config: relative path only, no `..`, optional `.pyi` suffix.
pub fn normalize_add_content_file(s: &str) -> anyhow::Result<PathBuf> {
    use anyhow::bail;
    let s = s.trim();
    if s.is_empty() {
        bail!("add_content.file must not be empty");
    }
    if s.starts_with('/') || s.contains('\\') {
        bail!("add_content.file must be a relative path using '/': {}", s);
    }
    let mut out = PathBuf::new();
    for seg in s.split('/') {
        if seg.is_empty() || seg == "." {
            continue;
        }
        if seg == ".." {
            bail!("add_content.file must not contain '..': {}", s);
        }
        out.push(seg);
    }
    if out.as_os_str().is_empty() {
        bail!("add_content.file is not a valid path: {}", s);
    }
    if out.extension().and_then(|e| e.to_str()) != Some("pyi") {
        out.set_extension("pyi");
    }
    Ok(out)
}

/// Compare two relative stub paths from layout vs config.
pub fn canonical_stub_rel_path(p: &Path) -> PathBuf {
    p.components().collect()
}

/// Every `add_content` entry must refer to a `.pyi` that is generated in this run.
pub fn validate_add_content_targets(
    entries: &[AddContentEntry],
    generated_rel_paths: &[PathBuf],
) -> anyhow::Result<()> {
    use anyhow::bail;
    if entries.is_empty() {
        return Ok(());
    }
    let set: HashSet<PathBuf> = generated_rel_paths
        .iter()
        .map(|p| canonical_stub_rel_path(p))
        .collect();
    for e in entries {
        let p = normalize_add_content_file(&e.file)?;
        let key = canonical_stub_rel_path(&p);
        if !set.contains(&key) {
            bail!(
                "add_content: no stub generated for file = {:?} (check -o and package layout)",
                e.file
            );
        }
    }
    Ok(())
}

/// Version-specific rendering decisions, computed once from the configured Python version
/// and threaded through all generation code.
///
/// Adding a new version-specific behavior means adding a field here and deriving it in
/// `from_version` — no changes needed elsewhere in the generation pipeline.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderPolicy {
    /// Emit `T | None` (py ≥ 3.10) instead of `t.Optional[T]` (py < 3.10).
    pub union_optional: bool,
    /// Emit `t.Self` (py ≥ 3.11, PEP 673) instead of the class name in stubs (`import typing as t`).
    /// When `false`, class names are used as forward references and
    /// `from __future__ import annotations` is added to the output.
    pub native_self: bool,
    /// Prepend `from __future__ import annotations` to the generated file.
    /// Needed when using class names as forward references (i.e. when `!native_self`).
    pub future_annotations: bool,
    /// Use PEP 585 style `list[T]`, `dict[K, V]`, `tuple[...]`, `set[T]` (py ≥ 3.9).
    /// When `false` (e.g. Python 3.8), emit `t.List`, `t.Dict`, `t.Tuple`, `t.Set` instead.
    pub pep585_builtin_generics: bool,
}

impl RenderPolicy {
    pub fn from_version(major: u32, minor: u32) -> Self {
        let v = (major, minor);
        let native_self = v >= (3, 11);
        Self {
            union_optional: v >= (3, 10),
            native_self,
            // forward-reference class names require lazy annotation evaluation
            future_annotations: !native_self,
            pep585_builtin_generics: v >= (3, 9),
        }
    }
}

/// Deep-merge two TOML values; keys in `override_val` take precedence over `base`.
/// Array tables (e.g. `[[override]]`) are replaced as a whole, not merged item-by-item.
fn deep_merge_toml(base: toml::Value, override_val: &toml::Value) -> toml::Value {
    use toml::Value;
    match (base, override_val) {
        (Value::Table(mut base_t), Value::Table(override_t)) => {
            for (k, ov) in override_t {
                let merged = base_t
                    .remove(k.as_str())
                    .map(|b| deep_merge_toml(b, ov))
                    .unwrap_or_else(|| ov.clone());
                base_t.insert(k.clone(), merged);
            }
            Value::Table(base_t)
        }
        (_, override_v) => override_v.clone(),
    }
}

/// Read `[tool.rylai]` from pyproject.toml if present.
fn tool_rylai_from_pyproject(path: &Path) -> Option<toml::Value> {
    let content = std::fs::read_to_string(path).ok()?;
    let doc: toml::Value = content.parse().ok()?;
    doc.get("tool").and_then(|t| t.get("rylai")).cloned()
}

impl Config {
    /// Load config by merging `[tool.rylai]` from pyproject.toml (base) and rylai.toml (override).
    /// When both exist, duplicate keys are resolved in favor of rylai.toml; otherwise both apply.
    pub fn load_merged(rylai_toml_path: &Path, pyproject_path: &Path) -> Result<Self> {
        let base = tool_rylai_from_pyproject(pyproject_path)
            .unwrap_or_else(|| toml::Value::Table(Default::default()));
        let override_val = if rylai_toml_path.exists() {
            let content = std::fs::read_to_string(rylai_toml_path)?;
            toml::from_str(&content).context("parse rylai.toml")?
        } else {
            toml::Value::Table(Default::default())
        };
        let merged = deep_merge_toml(base, &override_val);
        let config: Self = merged
            .try_into()
            .map_err(|e| anyhow::anyhow!("invalid config structure: {}", e))?;
        config.validate_overrides()?;
        Ok(config)
    }

    /// `[[override]]`: use `stub` alone, **or** at least one of non-empty `param_types` / `return_type`.
    /// `stub` must not appear together with `param_types` or `return_type`.
    pub fn validate_overrides(&self) -> Result<()> {
        use anyhow::bail;
        for (i, o) in self.overrides.iter().enumerate() {
            let has_stub = o.stub.as_ref().is_some_and(|s| !s.trim().is_empty());
            let has_pt = o.param_types.as_ref().is_some_and(|m| !m.is_empty());
            let has_rt = o.return_type.as_ref().is_some_and(|s| !s.trim().is_empty());

            if has_stub && (has_pt || has_rt) {
                bail!(
                    "[[override]] must not combine `stub` with `param_types` or `return_type` (entry index {i})"
                );
            }
            if !has_stub && !has_pt && !has_rt {
                bail!(
                    "[[override]] must set `stub`, or non-empty `param_types`, or non-empty `return_type` (entry index {i})"
                );
            }

            if has_pt {
                let mut seen: HashMap<String, String> = HashMap::new();
                for k in o.param_types.as_ref().expect("has_pt").keys() {
                    let n = normalize_param_types_key(k);
                    if let Some(first) = seen.get(&n) {
                        bail!(
                            "[[override]] param_types: duplicate keys after normalization (`{k}` and `{first}` → `{n}`) at entry index {i}"
                        );
                    }
                    seen.insert(n, k.clone());
                }
            }
        }
        Ok(())
    }

    /// Return the Python version as (major, minor) tuple for comparison.
    pub fn python_version_tuple(&self) -> (u32, u32) {
        let parts: Vec<u32> = self
            .output
            .python_version
            .split('.')
            .filter_map(|s| s.parse().ok())
            .collect();
        (
            parts.first().copied().unwrap_or(3),
            parts.get(1).copied().unwrap_or(10),
        )
    }

    /// Compute the rendering policy for the configured Python version.
    pub fn render_policy(&self) -> RenderPolicy {
        let (major, minor) = self.python_version_tuple();
        RenderPolicy::from_version(major, minor)
    }
}

/// Rust idents for which every `[type_map]` key sharing this last segment maps to the same Python type.
/// Used when expanding `type` aliases: only then is the alias name kept for [`crate::type_map::map_type`]
/// (e.g. `Vec<PyBbox>` stays tied to `PyBbox` instead of expanding to a tuple first).
pub fn type_map_preserve_alias_idents(type_map: &HashMap<String, String>) -> HashSet<String> {
    let mut by_last: HashMap<String, HashSet<String>> = HashMap::new();
    for (k, v) in type_map {
        let last = k.rsplit("::").next().unwrap().to_string();
        by_last.entry(last).or_default().insert(v.clone());
    }
    by_last
        .into_iter()
        .filter(|(_, vs)| vs.len() == 1)
        .map(|(ident, _)| ident)
        .collect()
}

/// Merge `[type_map]` entries into the pyclass-derived `known_classes` map used during stub generation.
/// Inserts full Rust path keys first (deterministic order), then short-name entries only when the
/// Python target is unambiguous for that last segment. Warns and keeps existing mappings on conflict.
pub(crate) fn merge_type_map_into_known_classes(
    known_classes: &HashMap<String, String>,
    type_map: &HashMap<String, String>,
) -> (HashMap<String, String>, Vec<String>) {
    let mut warnings = Vec::new();
    let mut m = known_classes.clone();

    let mut keys: Vec<String> = type_map.keys().cloned().collect();
    keys.sort();

    for k in &keys {
        let v = type_map.get(k).expect("key from sorted list");
        match m.get(k.as_str()) {
            None => {
                m.insert(k.clone(), v.clone());
            }
            Some(existing) if existing == v => {}
            Some(existing) => {
                warnings.push(format!(
                    "rylai: [type_map] key `{k}` conflicts with existing Rust→Python mapping (`{existing}` vs `{v}`); keeping existing"
                ));
            }
        }
    }

    let mut last_groups: HashMap<String, Vec<(String, String)>> = HashMap::new();
    for k in &keys {
        let v = type_map.get(k).expect("key from sorted list");
        let last = k.rsplit("::").next().unwrap().to_string();
        last_groups
            .entry(last)
            .or_default()
            .push((k.clone(), v.clone()));
    }

    let mut lasts: Vec<String> = last_groups.keys().cloned().collect();
    lasts.sort();

    for last in lasts {
        let entries = last_groups.get(&last).expect("last segment group");
        let distinct: HashSet<&str> = entries.iter().map(|(_, v)| v.as_str()).collect();
        if distinct.len() > 1 {
            let detail: Vec<String> = entries.iter().map(|(k, v)| format!("{k} → {v}")).collect();
            warnings.push(format!(
                "rylai: [type_map] ambiguous type name `{last}` (multiple Python targets): {}; not registering short name — use full Rust paths in config or disambiguate",
                detail.join(", ")
            ));
            continue;
        }
        let v = &entries[0].1;
        match m.get(&last) {
            None => {
                m.insert(last.clone(), v.clone());
            }
            Some(existing) if existing == v => {}
            Some(existing) => {
                warnings.push(format!(
                    "rylai: [type_map] short name `{last}` would map to `{v}` but existing mapping is `{existing}`; keeping existing"
                ));
            }
        }
    }

    (m, warnings)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn render_policy_py39_optional_no_native_self() {
        let p = RenderPolicy::from_version(3, 9);
        assert!(!p.union_optional, "3.9 uses t.Optional[T]");
        assert!(!p.native_self, "3.9 uses class name, not Self");
        assert!(
            p.future_annotations,
            "3.9 needs future annotations for forward refs"
        );
        assert!(
            p.pep585_builtin_generics,
            "3.9 has PEP 585 builtin generics"
        );
    }

    #[test]
    fn render_policy_py38_pep585_disabled() {
        let p = RenderPolicy::from_version(3, 8);
        assert!(!p.pep585_builtin_generics, "3.8 must use t.List etc.");
    }

    #[test]
    fn render_policy_py310_union_optional_no_native_self() {
        let p = RenderPolicy::from_version(3, 10);
        assert!(p.union_optional, "3.10 uses T | None");
        assert!(!p.native_self, "3.10 still uses class name");
        assert!(p.future_annotations);
    }

    #[test]
    fn render_policy_py311_native_self_no_future_annotations() {
        let p = RenderPolicy::from_version(3, 11);
        assert!(p.union_optional);
        assert!(p.native_self, "3.11 has typing.Self (PEP 673)");
        assert!(
            !p.future_annotations,
            "native Self does not require future annotations"
        );
    }

    #[test]
    fn render_policy_py312_same_as_py311() {
        let p = RenderPolicy::from_version(3, 12);
        assert!(p.union_optional);
        assert!(p.native_self);
        assert!(!p.future_annotations);
    }

    #[test]
    fn config_python_version_tuple_parses_major_minor() {
        let mut config = Config::default();
        config.output.python_version = "3.9".to_string();
        assert_eq!(config.python_version_tuple(), (3, 9));

        config.output.python_version = "3.12.0".to_string();
        assert_eq!(config.python_version_tuple(), (3, 12));
    }

    #[test]
    fn config_python_version_tuple_invalid_fallback() {
        let mut config = Config::default();
        config.output.python_version = "invalid".to_string();
        // Fallback: no successful parse → (3, 10) from defaults in implementation
        let (major, minor) = config.python_version_tuple();
        assert_eq!(major, 3);
        assert_eq!(minor, 10);
    }

    #[test]
    fn config_render_policy_uses_configured_version() {
        let mut config = Config::default();
        config.output.python_version = "3.9".to_string();
        let p = config.render_policy();
        assert!(!p.union_optional);
        assert!(p.future_annotations);
        assert!(p.pep585_builtin_generics);

        config.output.python_version = "3.12".to_string();
        let p = config.render_policy();
        assert!(p.native_self);
        assert!(!p.future_annotations);
    }

    // ── Config::load_merged ───────────────────────────────────────────────────

    #[test]
    fn load_merged_returns_default_when_both_absent() {
        let dir = tempfile::tempdir().expect("tempdir");
        let rylai_toml = dir.path().join("no_rylai_absent.toml");
        let pyproject = dir.path().join("no_pyproject_absent.toml");
        let config =
            Config::load_merged(&rylai_toml, &pyproject).expect("absent files must not error");
        assert_eq!(
            config.output.python_version, "3.10",
            "default python version"
        );
        assert!(config.output.add_header, "header enabled by default");
        assert!(config.type_map.is_empty(), "type_map empty by default");
        assert!(config.overrides.is_empty(), "overrides empty by default");
        assert!(
            config.add_content.is_empty(),
            "add_content empty by default"
        );
    }

    #[test]
    fn validate_overrides_rejects_both_stub_and_param_types() {
        let mut c = Config::default();
        c.overrides.push(OverrideEntry {
            item: "a::b".to_string(),
            stub: Some("def b(): ...".to_string()),
            param_types: Some(HashMap::from([("x".to_string(), "int".to_string())])),
            return_type: None,
        });
        let err = c.validate_overrides().unwrap_err();
        assert!(
            err.to_string().contains("combine") && err.to_string().contains("stub"),
            "expected stub+param_types error, got {err}"
        );
    }

    #[test]
    fn validate_overrides_rejects_neither_stub_nor_param_types_nor_return_type() {
        let mut c = Config::default();
        c.overrides.push(OverrideEntry {
            item: "a::b".to_string(),
            stub: None,
            param_types: None,
            return_type: None,
        });
        let err = c.validate_overrides().unwrap_err();
        assert!(
            err.to_string().contains("return_type") || err.to_string().contains("param_types"),
            "expected missing-fields error, got {err}"
        );
    }

    #[test]
    fn validate_overrides_rejects_stub_with_return_type() {
        let mut c = Config::default();
        c.overrides.push(OverrideEntry {
            item: "a::b".to_string(),
            stub: Some("def b(): ...".to_string()),
            param_types: None,
            return_type: Some("int".to_string()),
        });
        let err = c.validate_overrides().unwrap_err();
        assert!(
            err.to_string().contains("stub") && err.to_string().contains("return_type"),
            "expected stub+return_type error, got {err}"
        );
    }

    #[test]
    fn validate_overrides_accepts_return_type_only() {
        let mut c = Config::default();
        c.overrides.push(OverrideEntry {
            item: "a::b".to_string(),
            stub: None,
            param_types: None,
            return_type: Some("list[str]".to_string()),
        });
        c.validate_overrides().expect("return_type alone is valid");
    }

    #[test]
    fn validate_overrides_rejects_duplicate_param_types_keys_after_normalization() {
        let mut c = Config::default();
        c.overrides.push(OverrideEntry {
            item: "a::b".to_string(),
            stub: None,
            param_types: Some(HashMap::from([
                ("kwargs".to_string(), "t.Any".to_string()),
                ("**kwargs".to_string(), "Unpack[X]".to_string()),
            ])),
            return_type: None,
        });
        let err = c.validate_overrides().unwrap_err();
        assert!(
            err.to_string().contains("duplicate") || err.to_string().contains("normalization"),
            "expected duplicate key error, got {err}"
        );
    }

    #[test]
    fn load_merged_parses_valid_rylai_toml_only() {
        use std::io::Write;
        let dir = tempfile::tempdir().expect("tempdir");
        let rylai_toml = dir.path().join("rylai_merged_valid.toml");
        let pyproject = dir.path().join("pyproject_merged_nonexist.toml");
        {
            let mut f = std::fs::File::create(&rylai_toml).expect("create temp file");
            write!(
                f,
                r#"
[type_map]
"numpy::PyReadonlyArray1" = "numpy.ndarray"

[fallback]
strategy = "error"

[output]
python_version = "3.11"
add_header = false

[[override]]
item = "my_module::my_fn"
stub = "def my_fn() -> int: ..."
"#
            )
            .expect("write temp file");
        }

        let config = Config::load_merged(&rylai_toml, &pyproject).expect("valid toml must parse");

        assert_eq!(
            config
                .type_map
                .get("numpy::PyReadonlyArray1")
                .map(String::as_str),
            Some("numpy.ndarray")
        );
        assert_eq!(config.fallback.strategy, FallbackStrategy::Error);
        assert_eq!(config.output.python_version, "3.11");
        assert!(!config.output.add_header);
        assert_eq!(config.overrides.len(), 1);
        assert_eq!(config.overrides[0].item, "my_module::my_fn");
        assert_eq!(
            config.overrides[0].stub.as_deref(),
            Some("def my_fn() -> int: ...")
        );
    }

    #[test]
    fn load_merged_invalid_rylai_toml_errors() {
        use std::io::Write;
        let dir = tempfile::tempdir().expect("tempdir");
        let rylai_toml = dir.path().join("rylai_merged_invalid.toml");
        let pyproject = dir.path().join("pyproject_merged_nonexist2.toml");
        {
            let mut f = std::fs::File::create(&rylai_toml).expect("create temp file");
            write!(f, "this is not valid = [ toml").expect("write temp file");
        }

        let result = Config::load_merged(&rylai_toml, &pyproject);
        assert!(result.is_err(), "invalid rylai.toml must return Err");
        let err = result.unwrap_err();
        assert!(
            err.to_string().contains("parse rylai.toml") || err.to_string().contains("toml"),
            "error should mention rylai.toml or toml: {}",
            err
        );
    }

    #[test]
    fn load_merged_parses_features_from_rylai_toml() {
        use std::io::Write;
        let dir = tempfile::tempdir().expect("tempdir");
        let rylai_toml = dir.path().join("rylai_merged_features.toml");
        let pyproject = dir.path().join("pyproject_merged_nonexist3.toml");
        {
            let mut f = std::fs::File::create(&rylai_toml).expect("create temp file");
            write!(
                f,
                r#"
[features]
enabled = ["default", "extra"]
"#
            )
            .expect("write temp file");
        }

        let config = Config::load_merged(&rylai_toml, &pyproject).expect("valid toml must parse");
        assert_eq!(config.features.enabled, ["default", "extra"]);
    }

    #[test]
    fn load_merged_only_pyproject() {
        use std::io::Write;
        let dir = tempfile::tempdir().expect("tempdir");
        let pyproject = dir.path().join("pyproject_merge_test.toml");
        let rylai_toml = dir.path().join("rylai_merge_nonexist.toml");
        {
            let mut f = std::fs::File::create(&pyproject).expect("create");
            write!(
                f,
                r#"
[project]
name = "test"

[tool.rylai.output]
python_version = "3.9"
add_header = false

[tool.rylai.fallback]
strategy = "skip"
"#
            )
            .expect("write");
        }

        let config = Config::load_merged(&rylai_toml, &pyproject).expect("merge must succeed");
        assert_eq!(config.output.python_version, "3.9");
        assert!(!config.output.add_header);
        assert_eq!(config.fallback.strategy, FallbackStrategy::Skip);
    }

    #[test]
    fn load_merged_pyproject_parses_override_array() {
        use std::io::Write;
        let dir = tempfile::tempdir().expect("tempdir");
        let pyproject = dir.path().join("pyproject_override_array_test.toml");
        let rylai_toml = dir.path().join("rylai_nonexist_override_test.toml");
        {
            let mut f = std::fs::File::create(&pyproject).expect("create");
            write!(
                f,
                r#"
[project]
name = "test"

[tool.rylai.output]
python_version = "3.11"

[[tool.rylai.override]]
item = "my_module::complex_function"
stub = "def complex_function(x: t.Any, **kwargs: t.Any) -> dict[str, t.Any]: ..."

[[tool.rylai.override]]
item = "other::fn"
stub = "def fn() -> int: ..."
"#
            )
            .expect("write");
        }

        let config = Config::load_merged(&rylai_toml, &pyproject).expect("merge must succeed");
        assert_eq!(config.output.python_version, "3.11");
        assert_eq!(config.overrides.len(), 2);
        assert_eq!(config.overrides[0].item, "my_module::complex_function");
        assert_eq!(
            config.overrides[0].stub.as_deref(),
            Some("def complex_function(x: t.Any, **kwargs: t.Any) -> dict[str, t.Any]: ...")
        );
        assert_eq!(config.overrides[1].item, "other::fn");
        assert_eq!(
            config.overrides[1].stub.as_deref(),
            Some("def fn() -> int: ...")
        );
    }

    #[test]
    fn load_merged_parses_add_content_array() {
        use std::io::Write;
        let dir = tempfile::tempdir().expect("tempdir");
        let rylai_toml = dir.path().join("rylai_add_content.toml");
        let pyproject = dir.path().join("pyproject_absent_add_content.toml");
        {
            let mut f = std::fs::File::create(&rylai_toml).expect("create temp file");
            write!(
                f,
                r#"
[[add_content]]
file = "pkg/a.pyi"
location = "after-import-typing"
content = "X = 1\n"

[[add_content]]
file = "b.pyi"
location = "head"
content = "x"
"#
            )
            .expect("write temp file");
        }

        let config = Config::load_merged(&rylai_toml, &pyproject).expect("valid toml must parse");
        assert_eq!(config.add_content.len(), 2);
        assert_eq!(config.add_content[0].file, "pkg/a.pyi");
        assert_eq!(
            config.add_content[0].location,
            AddContentLocation::AfterImportTyping
        );
        assert_eq!(config.add_content[0].content, "X = 1\n");
        assert_eq!(config.add_content[1].location, AddContentLocation::Head);
        assert_eq!(config.add_content[1].content, "x");
    }

    #[test]
    fn normalize_add_content_file_appends_pyi() {
        assert_eq!(
            normalize_add_content_file("foo/bar").unwrap(),
            PathBuf::from("foo/bar.pyi")
        );
        assert_eq!(
            normalize_add_content_file("x.pyi").unwrap(),
            PathBuf::from("x.pyi")
        );
        assert!(normalize_add_content_file("../x").is_err());
        assert!(normalize_add_content_file("a\\b").is_err());
    }

    #[test]
    fn validate_add_content_targets_errors_when_missing() {
        let entries = vec![AddContentEntry {
            file: "only.pyi".to_string(),
            location: AddContentLocation::Tail,
            content: "pass\n".to_string(),
        }];
        let err =
            validate_add_content_targets(&entries, &[PathBuf::from("other.pyi")]).unwrap_err();
        assert!(
            err.to_string().contains("only.pyi") || err.to_string().contains("no stub"),
            "got: {err}"
        );
    }

    #[test]
    fn validate_add_content_targets_ok_when_matched() {
        let entries = vec![AddContentEntry {
            file: "pkg/a.pyi".to_string(),
            location: AddContentLocation::Tail,
            content: "pass\n".to_string(),
        }];
        validate_add_content_targets(&entries, &[PathBuf::from("pkg").join("a.pyi")])
            .expect("same logical path");
    }

    #[test]
    fn load_merged_parses_format_commands() {
        use std::io::Write;
        let dir = tempfile::tempdir().expect("tempdir");
        let rylai_toml = dir.path().join("rylai_format_test.toml");
        let pyproject = dir.path().join("pyproject_nonexist.toml");
        {
            let mut f = std::fs::File::create(&rylai_toml).expect("create");
            write!(
                f,
                r#"
format = ["ruff format", "ruff check --select I --fix", "black"]
"#
            )
            .expect("write");
        }

        let config = Config::load_merged(&rylai_toml, &pyproject).expect("valid toml must parse");
        assert_eq!(config.format.len(), 3);
        assert_eq!(config.format[0], "ruff format");
        assert_eq!(config.format[1], "ruff check --select I --fix");
        assert_eq!(config.format[2], "black");
    }

    #[test]
    fn load_merged_rylai_toml_overrides_duplicate_keys() {
        use std::io::Write;
        let dir = tempfile::tempdir().expect("tempdir");
        let pyproject = dir.path().join("pyproject_override_test.toml");
        let rylai_toml = dir.path().join("rylai_override_test.toml");
        {
            let mut f = std::fs::File::create(&pyproject).expect("create");
            write!(
                f,
                r#"
[project]
name = "test"

[tool.rylai.output]
python_version = "3.9"
add_header = true

[tool.rylai.type_map]
"foo::Bar" = "bar.Foo"
"only::Pyproject" = "pyproject.Only"
"#
            )
            .expect("write");
        }
        {
            let mut f = std::fs::File::create(&rylai_toml).expect("create");
            write!(
                f,
                r#"
[output]
python_version = "3.12"
add_header = false

[type_map]
"foo::Bar" = "rylai.Wins"
"only::Rylai" = "rylai.Only"
"#
            )
            .expect("write");
        }

        let config = Config::load_merged(&rylai_toml, &pyproject).expect("merge must succeed");
        // Duplicate keys: rylai.toml wins
        assert_eq!(config.output.python_version, "3.12");
        assert!(!config.output.add_header);
        assert_eq!(
            config.type_map.get("foo::Bar").map(String::as_str),
            Some("rylai.Wins")
        );
        // Non-duplicate: both apply
        assert_eq!(
            config.type_map.get("only::Pyproject").map(String::as_str),
            Some("pyproject.Only")
        );
        assert_eq!(
            config.type_map.get("only::Rylai").map(String::as_str),
            Some("rylai.Only")
        );
    }

    #[test]
    fn type_map_preserve_alias_idents_requires_unambiguous_last_segment() {
        let mut m = HashMap::new();
        m.insert("a::Foo".to_string(), "X".to_string());
        m.insert("b::Foo".to_string(), "Y".to_string());
        let idents = type_map_preserve_alias_idents(&m);
        assert!(!idents.contains("Foo"));

        let mut m2 = HashMap::new();
        m2.insert("a::Foo".to_string(), "X".to_string());
        m2.insert("b::Foo".to_string(), "X".to_string());
        let idents2 = type_map_preserve_alias_idents(&m2);
        assert!(idents2.contains("Foo"));
    }

    #[test]
    fn merge_type_map_into_known_classes_warns_and_skips_ambiguous_short_name() {
        let known = HashMap::new();
        let mut tm = HashMap::new();
        tm.insert("a::Foo".to_string(), "P".to_string());
        tm.insert("b::Foo".to_string(), "Q".to_string());
        tm.insert("crate::Bar".to_string(), "BarT".to_string());

        let (m, w) = merge_type_map_into_known_classes(&known, &tm);
        assert!(!m.contains_key("Foo"));
        assert!(w.iter().any(|s| s.contains("ambiguous")));
        assert_eq!(m.get("a::Foo").map(String::as_str), Some("P"));
        assert_eq!(m.get("crate::Bar").map(String::as_str), Some("BarT"));
        assert_eq!(m.get("Bar").map(String::as_str), Some("BarT"));
    }

    #[test]
    fn merge_type_map_into_known_classes_keeps_known_class_on_short_name_conflict() {
        let mut known = HashMap::new();
        known.insert("Foo".to_string(), "ExistingPy".to_string());
        let mut tm = HashMap::new();
        tm.insert("x::Foo".to_string(), "NewT".to_string());
        let (m, w) = merge_type_map_into_known_classes(&known, &tm);
        assert_eq!(m.get("Foo").map(String::as_str), Some("ExistingPy"));
        assert_eq!(m.get("x::Foo").map(String::as_str), Some("NewT"));
        assert!(w.iter().any(|s| s.contains("short name")));
    }
}
