//! LOT D0 (fondation §17.1) — tests purs du domaine :
//! - `StructuredAdapter` : round-trip serde camelCase (`"claude"` / `"codex"`) ;
//! - `AgentProfile.structured_adapter` : zéro régression serde (omission via
//!   `skip_serializing_if`, défaut `None` à la désérialisation), round-trip avec
//!   adapter, et builder `with_structured_adapter` qui ne touche rien d'autre ;
//! - types du port `AgentSession` : `ReplyEvent` (3 variantes + égalité),
//!   `AgentSessionError` (variantes + `Display`/`Error`), et un **fake in-module**
//!   prouvant que `AgentSession`/`AgentSessionFactory` sont implémentables (D0 = pas
//!   d'impl réelle, seulement la conformité de signature).
//!
//! Style calqué sur `serde_roundtrip.rs` / `agent_profile_a0.rs` (réutilise les
//! constructeurs validants du domaine, pas de littéraux fragiles).

use std::sync::Arc;

use async_trait::async_trait;

use domain::ids::{ProfileId, SessionId};
use domain::ports::{
    AgentSession, AgentSessionError, AgentSessionFactory, PreparedContext, ReplyEvent, ReplyStream,
    SessionPlan,
};
use domain::profile::{AgentProfile, ContextInjection, StructuredAdapter};
use domain::project::ProjectPath;
use domain::MarkdownDoc;
use uuid::Uuid;

fn profid(n: u128) -> ProfileId {
    ProfileId::from_uuid(Uuid::from_u128(n))
}

fn roundtrip<T>(value: &T) -> T
where
    T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
    let json = serde_json::to_string(value).expect("serialize");
    serde_json::from_str(&json).expect("deserialize")
}

// ---------------------------------------------------------------------------
// StructuredAdapter — round-trip serde camelCase ("claude" / "codex")
// ---------------------------------------------------------------------------

#[test]
fn structured_adapter_serialises_camel_case() {
    assert_eq!(
        serde_json::to_string(&StructuredAdapter::Claude).unwrap(),
        "\"claude\""
    );
    assert_eq!(
        serde_json::to_string(&StructuredAdapter::Codex).unwrap(),
        "\"codex\""
    );
}

#[test]
fn structured_adapter_deserialises_from_camel_case() {
    let claude: StructuredAdapter = serde_json::from_str("\"claude\"").unwrap();
    let codex: StructuredAdapter = serde_json::from_str("\"codex\"").unwrap();
    assert_eq!(claude, StructuredAdapter::Claude);
    assert_eq!(codex, StructuredAdapter::Codex);
}

#[test]
fn structured_adapter_roundtrips_both_variants() {
    for adapter in [StructuredAdapter::Claude, StructuredAdapter::Codex] {
        assert_eq!(roundtrip(&adapter), adapter);
    }
}

#[test]
fn structured_adapter_rejects_unknown_variant() {
    // Un JSON hors vocabulaire ne doit pas se faufiler (pas de défaut silencieux).
    let parsed: Result<StructuredAdapter, _> = serde_json::from_str("\"gemini\"");
    assert!(parsed.is_err(), "unknown adapter must fail to deserialise");
}

// ---------------------------------------------------------------------------
// AgentProfile.structured_adapter — zéro régression serde
// ---------------------------------------------------------------------------

/// Profil minimal TUI/PTY (sans adapter), construit via le constructeur validant.
fn pty_profile() -> AgentProfile {
    AgentProfile::new(
        profid(1),
        "Gemini CLI",
        "gemini",
        vec![],
        ContextInjection::convention_file("GEMINI.md").unwrap(),
        Some("gemini --version".to_owned()),
        "{agentRunDir}",
        None,
    )
    .unwrap()
}

#[test]
fn profile_without_adapter_omits_the_field() {
    // skip_serializing_if = Option::is_none ⇒ la clé `structuredAdapter` est ABSENTE.
    let p = pty_profile();
    assert_eq!(p.structured_adapter, None);
    let json = serde_json::to_string(&p).unwrap();
    assert!(
        !json.contains("structuredAdapter"),
        "field must be omitted when None; json was {json}"
    );
    // Et le round-trip reste fidèle.
    assert_eq!(roundtrip(&p), p);
}

#[test]
fn legacy_profile_without_adapter_deserialises_to_none() {
    // Un `profiles.json` produit AVANT que le champ existe : pas de clé
    // `structuredAdapter` ⇒ défaut `None` (zéro régression de désérialisation).
    let json = r#"{
        "id": "00000000-0000-0000-0000-000000000001",
        "name": "Gemini CLI",
        "command": "gemini",
        "args": [],
        "contextInjection": { "strategy": "conventionFile", "target": "GEMINI.md" },
        "detect": null,
        "cwdTemplate": "{agentRunDir}"
    }"#;
    let p: AgentProfile = serde_json::from_str(json).expect("legacy profile must deserialise");
    assert_eq!(p.structured_adapter, None);
}

#[test]
fn profile_with_adapter_roundtrips_and_uses_camel_case() {
    let p = pty_profile().with_structured_adapter(StructuredAdapter::Claude);
    let json = serde_json::to_string(&p).unwrap();
    // Clé camelCase + valeur camelCase de la variante.
    assert!(
        json.contains("\"structuredAdapter\":\"claude\""),
        "json was {json}"
    );
    assert!(!json.contains("structured_adapter"), "json was {json}");
    assert_eq!(roundtrip(&p), p);

    // Codex aussi.
    let c = pty_profile().with_structured_adapter(StructuredAdapter::Codex);
    let json = serde_json::to_string(&c).unwrap();
    assert!(
        json.contains("\"structuredAdapter\":\"codex\""),
        "json was {json}"
    );
    assert_eq!(roundtrip(&c), c);
}

#[test]
fn with_structured_adapter_sets_only_that_field() {
    let before = pty_profile();
    let after = before
        .clone()
        .with_structured_adapter(StructuredAdapter::Codex);

    // Le seul champ muté :
    assert_eq!(after.structured_adapter, Some(StructuredAdapter::Codex));
    assert_ne!(after.structured_adapter, before.structured_adapter);

    // Tous les autres champs strictement inchangés :
    assert_eq!(after.id, before.id);
    assert_eq!(after.name, before.name);
    assert_eq!(after.command, before.command);
    assert_eq!(after.args, before.args);
    assert_eq!(after.context_injection, before.context_injection);
    assert_eq!(after.detect, before.detect);
    assert_eq!(after.cwd_template, before.cwd_template);
    assert_eq!(after.session, before.session);

    // Preuve d'équivalence : ne diffère du `before` que par l'adapter posé.
    let mut patched = before;
    patched.structured_adapter = Some(StructuredAdapter::Codex);
    assert_eq!(after, patched);
}

#[test]
fn with_structured_adapter_is_last_write_wins() {
    // Re-poser un adapter écrase le précédent (idempotent par valeur).
    let p = pty_profile()
        .with_structured_adapter(StructuredAdapter::Claude)
        .with_structured_adapter(StructuredAdapter::Codex);
    assert_eq!(p.structured_adapter, Some(StructuredAdapter::Codex));
}

#[test]
fn new_defaults_structured_adapter_to_none() {
    // Le constructeur `new` reste stable : il ne pose jamais d'adapter.
    assert_eq!(pty_profile().structured_adapter, None);
}

// ---------------------------------------------------------------------------
// ReplyEvent — 3 variantes construites, égalité, clone
// ---------------------------------------------------------------------------

#[test]
fn reply_event_three_variants_construct_and_carry_payload() {
    let delta = ReplyEvent::TextDelta {
        text: "hel".to_owned(),
    };
    let tool = ReplyEvent::ToolActivity {
        label: "reads a file".to_owned(),
    };
    let final_ = ReplyEvent::Final {
        content: "hello world".to_owned(),
    };

    match &delta {
        ReplyEvent::TextDelta { text } => assert_eq!(text, "hel"),
        other => panic!("expected TextDelta, got {other:?}"),
    }
    match &tool {
        ReplyEvent::ToolActivity { label } => assert_eq!(label, "reads a file"),
        other => panic!("expected ToolActivity, got {other:?}"),
    }
    match &final_ {
        ReplyEvent::Final { content } => assert_eq!(content, "hello world"),
        other => panic!("expected Final, got {other:?}"),
    }
}

#[test]
fn reply_event_equality_and_clone() {
    let e = ReplyEvent::Final {
        content: "done".to_owned(),
    };
    assert_eq!(e.clone(), e);

    // Même variante, payload différent ⇒ inégal.
    assert_ne!(
        e,
        ReplyEvent::Final {
            content: "other".to_owned()
        }
    );
    // Variantes différentes ⇒ inégal.
    assert_ne!(
        ReplyEvent::TextDelta { text: "x".into() },
        ReplyEvent::ToolActivity { label: "x".into() }
    );
}

// ---------------------------------------------------------------------------
// AgentSessionError — variantes + Display / std::error::Error
// ---------------------------------------------------------------------------

#[test]
fn agent_session_error_display_messages() {
    assert_eq!(
        AgentSessionError::Start("cli missing".to_owned()).to_string(),
        "agent session start failed: cli missing"
    );
    assert_eq!(
        AgentSessionError::Io("broken pipe".to_owned()).to_string(),
        "agent session io failed: broken pipe"
    );
    assert_eq!(
        AgentSessionError::Decode("bad json".to_owned()).to_string(),
        "agent session decode failed: bad json"
    );
    assert_eq!(
        AgentSessionError::Timeout.to_string(),
        "agent session reply timed out"
    );
}

#[test]
fn agent_session_error_is_std_error_and_equates() {
    // Conformité au trait std::error::Error (thiserror).
    fn assert_error<E: std::error::Error>(_e: &E) {}
    let e = AgentSessionError::Decode("x".to_owned());
    assert_error(&e);

    // Clone + égalité par variante/payload.
    assert_eq!(e.clone(), e);
    assert_ne!(
        AgentSessionError::Start("a".into()),
        AgentSessionError::Start("b".into())
    );
    assert_ne!(
        AgentSessionError::Timeout,
        AgentSessionError::Io("t".into())
    );
}

// ---------------------------------------------------------------------------
// AgentSession / AgentSessionFactory — fake in-module (conformité de signature)
//
// D0 ne fournit PAS d'impl réelle : ce fake prouve seulement que les traits sont
// implémentables (object-safe, signatures cohérentes). Aucune logique réelle.
// ---------------------------------------------------------------------------

struct FakeSession {
    id: SessionId,
    conversation_id: Option<String>,
}

#[async_trait]
impl AgentSession for FakeSession {
    fn id(&self) -> SessionId {
        self.id
    }

    fn conversation_id(&self) -> Option<String> {
        self.conversation_id.clone()
    }

    async fn send(&self, prompt: &str) -> Result<ReplyStream, AgentSessionError> {
        // Flux borné minimal : un delta puis le Final déterministe.
        let stream: ReplyStream = Box::new(
            vec![
                ReplyEvent::TextDelta {
                    text: prompt.to_owned(),
                },
                ReplyEvent::Final {
                    content: prompt.to_owned(),
                },
            ]
            .into_iter(),
        );
        Ok(stream)
    }

    async fn shutdown(&self) -> Result<(), AgentSessionError> {
        Ok(())
    }
}

struct FakeFactory;

#[async_trait]
impl AgentSessionFactory for FakeFactory {
    fn supports(&self, profile: &AgentProfile) -> bool {
        profile.structured_adapter.is_some()
    }

    async fn start(
        &self,
        _profile: &AgentProfile,
        _ctx: &PreparedContext,
        _cwd: &ProjectPath,
        _session: &SessionPlan,
        _requester: Option<&str>,
        _env: &[(String, String)],
        _sandbox: Option<&domain::sandbox::SandboxPlan>,
    ) -> Result<Arc<dyn AgentSession>, AgentSessionError> {
        Ok(Arc::new(FakeSession {
            id: SessionId::from_uuid(Uuid::from_u128(7)),
            conversation_id: None,
        }))
    }
}

#[tokio::test]
async fn fake_session_proves_trait_is_implementable() {
    let sid = SessionId::from_uuid(Uuid::from_u128(42));
    let session = FakeSession {
        id: sid,
        conversation_id: Some("conv-1".to_owned()),
    };

    // Consommé comme trait-objet (object-safety du port via #[async_trait]).
    let dyn_session: Arc<dyn AgentSession> = Arc::new(session);
    assert_eq!(dyn_session.id(), sid);
    assert_eq!(dyn_session.conversation_id(), Some("conv-1".to_owned()));

    // send -> flux d'événements borné se terminant par Final (contrat §17.1).
    let events: Vec<ReplyEvent> = dyn_session.send("ping").await.unwrap().collect();
    assert_eq!(
        events,
        vec![
            ReplyEvent::TextDelta {
                text: "ping".to_owned()
            },
            ReplyEvent::Final {
                content: "ping".to_owned()
            },
        ]
    );
    assert!(matches!(events.last(), Some(ReplyEvent::Final { .. })));

    dyn_session.shutdown().await.expect("shutdown ok");
}

#[tokio::test]
async fn fake_factory_supports_only_structured_profiles_and_starts() {
    let factory: Arc<dyn AgentSessionFactory> = Arc::new(FakeFactory);

    let structured = pty_profile().with_structured_adapter(StructuredAdapter::Claude);
    let pty = pty_profile();
    assert!(factory.supports(&structured));
    assert!(!factory.supports(&pty));

    let ctx = PreparedContext {
        content: MarkdownDoc::new("# ctx"),
        relative_path: "CLAUDE.md".to_owned(),
        project_root: "/srv/project".to_owned(),
    };
    let cwd = ProjectPath::new("/srv/run").unwrap();
    let session = factory
        .start(&structured, &ctx, &cwd, &SessionPlan::None, None, &[], None)
        .await
        .expect("factory starts a session");
    assert_eq!(session.id(), SessionId::from_uuid(Uuid::from_u128(7)));
}
