//! Tests de integración del módulo `crm`. Mismo kernel que
//! inventory/sales/treasury, apuntado a `modules/crm`: clientes,
//! oportunidades que recorren un pipeline de ventas, e interacciones.

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

use nakui_core::executor::{ExecError, Executor};
use nakui_core::store::{MemoryStore, Store};
use serde_json::{json, Value};
use uuid::Uuid;

fn crm_module() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("dir del módulo nakui sobre core/")
        .join("modules/crm")
}

fn seed_cliente(store: &mut MemoryStore, id: Uuid, nombre: &str) {
    store.seed(
        "Cliente",
        id,
        json!({
            "id": id.to_string(),
            "nombre": nombre,
            "email": "contacto@example.com",
            "empresa": nombre,
        }),
    );
}

/// Abre una oportunidad y devuelve su id. Camino feliz (panica si falla).
fn abrir_opp(exec: &Executor, store: &mut MemoryStore, cliente: Uuid) -> Uuid {
    let opp = Uuid::new_v4();
    exec.run(
        store,
        "abrir_oportunidad",
        &[("cliente", cliente)],
        json!({
            "oportunidad_id": opp.to_string(),
            "titulo": "Licencia anual",
            "monto": 12_000_i64,
            "currency": "USD",
            "timestamp": "2026-05-21T10:00:00Z",
        }),
    )
    .expect("abrir_oportunidad debe pasar");
    opp
}

fn etapa(store: &MemoryStore, opp: Uuid) -> String {
    store
        .load("Oportunidad", opp)
        .and_then(|v| v.get("etapa").and_then(Value::as_str).map(String::from))
        .expect("oportunidad con etapa")
}

/// Corre `mover_oportunidad`; devuelve el conteo de ops en éxito.
// `ExecError` es un enum grande — el resto del crate convive con este
// lint; lo suprimimos local en vez de boxear sólo este helper.
#[allow(clippy::result_large_err)]
fn mover(
    exec: &Executor,
    store: &mut MemoryStore,
    opp: Uuid,
    destino: &str,
) -> Result<usize, ExecError> {
    exec.run(
        store,
        "mover_oportunidad",
        &[("oportunidad", opp)],
        json!({ "etapa": destino, "timestamp": "2026-05-21T11:00:00Z" }),
    )
    .map(|ops| ops.len())
}

#[test]
fn abrir_crea_oportunidad_en_prospecto() {
    let exec = Executor::load_module(crm_module()).expect("load module");
    let mut store = MemoryStore::new();
    let cliente = Uuid::new_v4();
    seed_cliente(&mut store, cliente, "Acme Corp");

    let opp = abrir_opp(&exec, &mut store, cliente);

    assert_eq!(etapa(&store, opp), "prospecto", "nace en prospecto");
    let o = store.load("Oportunidad", opp).expect("oportunidad existe");
    let cid = cliente.to_string();
    assert_eq!(
        o.get("cliente_id").and_then(Value::as_str),
        Some(cid.as_str())
    );
    assert_eq!(o.get("monto").and_then(Value::as_i64), Some(12_000));
}

#[test]
fn pipeline_avanza_hasta_ganada() {
    let exec = Executor::load_module(crm_module()).expect("load module");
    let mut store = MemoryStore::new();
    let cliente = Uuid::new_v4();
    seed_cliente(&mut store, cliente, "Acme Corp");
    let opp = abrir_opp(&exec, &mut store, cliente);

    for destino in ["calificado", "propuesta", "negociacion", "ganada"] {
        mover(&exec, &mut store, opp, destino)
            .unwrap_or_else(|e| panic!("mover a {destino} debe pasar: {e:?}"));
        assert_eq!(etapa(&store, opp), destino);
    }
}

#[test]
fn no_se_retrocede_en_el_pipeline() {
    let exec = Executor::load_module(crm_module()).expect("load module");
    let mut store = MemoryStore::new();
    let cliente = Uuid::new_v4();
    seed_cliente(&mut store, cliente, "Acme Corp");
    let opp = abrir_opp(&exec, &mut store, cliente);

    mover(&exec, &mut store, opp, "propuesta").expect("avanzar debe pasar");

    // prospecto está antes de propuesta → retroceso, rechazado por el script.
    let result = mover(&exec, &mut store, opp, "prospecto");
    match result {
        Err(ExecError::Rhai(_)) => {}
        other => panic!("esperaba Rhai (throw por retroceso), obtuve {other:?}"),
    }
    assert_eq!(etapa(&store, opp), "propuesta", "la etapa no cambió");
}

#[test]
fn oportunidad_cerrada_no_se_mueve() {
    let exec = Executor::load_module(crm_module()).expect("load module");
    let mut store = MemoryStore::new();
    let cliente = Uuid::new_v4();
    seed_cliente(&mut store, cliente, "Acme Corp");
    let opp = abrir_opp(&exec, &mut store, cliente);

    // Cerrar es legal desde cualquier etapa abierta.
    mover(&exec, &mut store, opp, "ganada").expect("cerrar debe pasar");

    // Una oportunidad ganada ya no se mueve.
    let result = mover(&exec, &mut store, opp, "negociacion");
    match result {
        Err(ExecError::Rhai(_)) => {}
        other => panic!("esperaba Rhai (throw por cerrada), obtuve {other:?}"),
    }
    assert_eq!(etapa(&store, opp), "ganada");
}

#[test]
fn etapa_destino_desconocida_es_rechazada() {
    let exec = Executor::load_module(crm_module()).expect("load module");
    let mut store = MemoryStore::new();
    let cliente = Uuid::new_v4();
    seed_cliente(&mut store, cliente, "Acme Corp");
    let opp = abrir_opp(&exec, &mut store, cliente);

    let result = mover(&exec, &mut store, opp, "facturada");
    assert!(matches!(result, Err(ExecError::Rhai(_))));
    assert_eq!(etapa(&store, opp), "prospecto");
}

#[test]
fn monto_negativo_es_rechazado() {
    let exec = Executor::load_module(crm_module()).expect("load module");
    let mut store = MemoryStore::new();
    let cliente = Uuid::new_v4();
    seed_cliente(&mut store, cliente, "Acme Corp");

    let opp = Uuid::new_v4();
    let result = exec.run(
        &mut store,
        "abrir_oportunidad",
        &[("cliente", cliente)],
        json!({
            "oportunidad_id": opp.to_string(),
            "titulo": "Trato inválido",
            "monto": -500_i64,
            "currency": "USD",
            "timestamp": "2026-05-21T10:00:00Z",
        }),
    );
    assert!(matches!(result, Err(ExecError::Rhai(_))));
    assert!(store.load("Oportunidad", opp).is_none(), "no se creó nada");
}

#[test]
fn registrar_interaccion_crea_registro() {
    let exec = Executor::load_module(crm_module()).expect("load module");
    let mut store = MemoryStore::new();
    let cliente = Uuid::new_v4();
    seed_cliente(&mut store, cliente, "Acme Corp");

    let int_id = Uuid::new_v4();
    exec.run(
        &mut store,
        "registrar_interaccion",
        &[("cliente", cliente)],
        json!({
            "interaccion_id": int_id.to_string(),
            "canal": "llamada",
            "nota": "Primer contacto, interés alto",
            "timestamp": "2026-05-21T09:00:00Z",
        }),
    )
    .expect("registrar_interaccion debe pasar");

    let i = store
        .load("Interaccion", int_id)
        .expect("interacción existe");
    assert_eq!(i.get("canal").and_then(Value::as_str), Some("llamada"));
    let cid = cliente.to_string();
    assert_eq!(
        i.get("cliente_id").and_then(Value::as_str),
        Some(cid.as_str())
    );
}

#[test]
fn canal_invalido_es_rechazado() {
    let exec = Executor::load_module(crm_module()).expect("load module");
    let mut store = MemoryStore::new();
    let cliente = Uuid::new_v4();
    seed_cliente(&mut store, cliente, "Acme Corp");

    let int_id = Uuid::new_v4();
    let result = exec.run(
        &mut store,
        "registrar_interaccion",
        &[("cliente", cliente)],
        json!({
            "interaccion_id": int_id.to_string(),
            "canal": "paloma-mensajera",
            "nota": "canal inexistente",
            "timestamp": "2026-05-21T09:00:00Z",
        }),
    );
    assert!(matches!(result, Err(ExecError::Rhai(_))));
    assert!(store.load("Interaccion", int_id).is_none());
}
