//! T7 tests for the [`InspectConversation`] use case (best-effort conversation
//! inspection feeding the resume popup).
//!
//! Each port is faked in-memory:
//! - [`FakeContexts`] — an [`AgentContextStore`] holding a manifest seeded with
//!   one agent,
//! - [`FakeProfiles`] — a [`ProfileStore`] returning a fixed profile list,
//! - [`FakeInspector`] — a [`SessionInspector`] whose `supports`/`details` are
//!   configurable, so we can exercise: a supporting inspector returning details,
//!   one returning `NotFound`, and the empty-`Vec` (no inspector) path.
//!
//! The contract under test (CONTEXT §T7 Part A): inspection is **best-effort** —
//! any miss (no inspector, unsupported profile, `NotFound`/`Read`) degrades to
//! empty details, never an error; only a genuine store failure errors.

use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use domain::agent::{Agent, AgentManifest, AgentOrigin, ManifestEntry};
use domain::ids::{AgentId, ProfileId, ProjectId};
use domain::markdown::MarkdownDoc;
use domain::ports::{
    AgentContextStore, ConversationDetails, InspectError, ProfileStore, SessionInspector,
    StoreError,
};
use domain::profile::{AgentProfile, ContextInjection};
use domain::project::{Project, ProjectPath};
use domain::remote::RemoteRef;
use uuid::Uuid;

use application::{InspectConversation, InspectConversationInput};

// ---------------------------------------------------------------------------
// FakeContexts (AgentContextStore) — only load_manifest is exercised here
// ---------------------------------------------------------------------------

#[derive(Clone)]
struct FakeContexts(Arc<Mutex<AgentManifest>>);

impl FakeContexts {
    fn with_agent(agent: &Agent) -> Self {
        Self(Arc::new(Mutex::new(AgentManifest {
            version: 1,
            entries: vec![ManifestEntry::from_agent(agent)],
            orchestrator: None,
        })))
    }
    fn empty() -> Self {
        Self(Arc::new(Mutex::new(AgentManifest {
            version: 1,
            entries: Vec::new(),
            orchestrator: None,
        })))
    }
}

#[async_trait]
impl AgentContextStore for FakeContexts {
    async fn read_context(
        &self,
        _project: &Project,
        _agent: &AgentId,
    ) -> Result<MarkdownDoc, StoreError> {
        Ok(MarkdownDoc::new(""))
    }
    async fn write_context(
        &self,
        _project: &Project,
        _agent: &AgentId,
        _md: &MarkdownDoc,
    ) -> Result<(), StoreError> {
        Ok(())
    }
    async fn load_manifest(&self, _project: &Project) -> Result<AgentManifest, StoreError> {
        Ok(self.0.lock().unwrap().clone())
    }
    async fn save_manifest(
        &self,
        _project: &Project,
        manifest: &AgentManifest,
    ) -> Result<(), StoreError> {
        *self.0.lock().unwrap() = manifest.clone();
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// FakeProfiles (ProfileStore)
// ---------------------------------------------------------------------------

#[derive(Clone)]
struct FakeProfiles(Arc<Vec<AgentProfile>>);

#[async_trait]
impl ProfileStore for FakeProfiles {
    async fn list(&self) -> Result<Vec<AgentProfile>, StoreError> {
        Ok((*self.0).clone())
    }
    async fn save(&self, _profile: &AgentProfile) -> Result<(), StoreError> {
        Ok(())
    }
    async fn delete(&self, _id: ProfileId) -> Result<(), StoreError> {
        Ok(())
    }
    async fn is_configured(&self) -> Result<bool, StoreError> {
        Ok(true)
    }
    async fn mark_configured(&self) -> Result<(), StoreError> {
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// FakeInspector (SessionInspector) — configurable support + result
// ---------------------------------------------------------------------------

/// What the fake inspector returns from `details`.
#[derive(Clone)]
enum InspectResult {
    Ok(ConversationDetails),
    Err(InspectError),
}

/// Shared probe recording the `(conversation_id, cwd)` an inspection was asked
/// for (so a test can assert the run dir was routed, not the project root).
type SeenProbe = Arc<Mutex<Option<(String, String)>>>;

struct FakeInspector {
    supports: bool,
    result: InspectResult,
    seen: SeenProbe,
}

impl FakeInspector {
    fn new(supports: bool, result: InspectResult) -> (Arc<Self>, SeenProbe) {
        let seen = Arc::new(Mutex::new(None));
        let me = Arc::new(Self {
            supports,
            result,
            seen: Arc::clone(&seen),
        });
        (me, seen)
    }
}

#[async_trait]
impl SessionInspector for FakeInspector {
    fn supports(&self, _profile: &AgentProfile) -> bool {
        self.supports
    }
    async fn details(
        &self,
        _profile: &AgentProfile,
        conversation_id: &str,
        cwd: &ProjectPath,
    ) -> Result<ConversationDetails, InspectError> {
        *self.seen.lock().unwrap() = Some((conversation_id.to_owned(), cwd.as_str().to_owned()));
        match &self.result {
            InspectResult::Ok(d) => Ok(d.clone()),
            InspectResult::Err(e) => Err(e.clone()),
        }
    }
}

// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------

fn project() -> Project {
    Project::new(
        ProjectId::from_uuid(Uuid::from_u128(1000)),
        "demo",
        ProjectPath::new("/home/me/proj").unwrap(),
        RemoteRef::local(),
        1_700_000_000_000,
    )
    .unwrap()
}

fn profile(id: ProfileId) -> AgentProfile {
    AgentProfile::new(
        id,
        "Claude Code",
        "claude",
        Vec::new(),
        ContextInjection::ConventionFile {
            target: "CLAUDE.md".to_owned(),
        },
        Some("claude --version".to_owned()),
        "{agentRunDir}",
        None,
    )
    .unwrap()
}

fn agent(id: AgentId, profile_id: ProfileId) -> Agent {
    Agent::new(
        id,
        "Bob",
        "agents/bob.md",
        profile_id,
        AgentOrigin::Scratch,
        false,
    )
    .unwrap()
}

fn input(agent_id: AgentId) -> InspectConversationInput {
    InspectConversationInput {
        project: project(),
        agent_id,
        conversation_id: "conv-123".to_owned(),
    }
}

// ---------------------------------------------------------------------------
// (a) supporting inspector → details propagated
// ---------------------------------------------------------------------------

#[tokio::test]
async fn supporting_inspector_propagates_details() {
    let pid = ProfileId::from_uuid(Uuid::from_u128(7));
    let aid = AgentId::from_uuid(Uuid::from_u128(42));
    let a = agent(aid, pid);

    let (inspector, seen) = FakeInspector::new(
        true,
        InspectResult::Ok(ConversationDetails {
            last_topic: Some("refactor the parser".to_owned()),
            token_count: Some(4242),
        }),
    );

    let uc = InspectConversation::new(
        Arc::new(FakeContexts::with_agent(&a)),
        Arc::new(FakeProfiles(Arc::new(vec![profile(pid)]))),
        vec![inspector],
    );

    let out = uc.execute(input(aid)).await.unwrap();
    assert_eq!(
        out.details.last_topic.as_deref(),
        Some("refactor the parser")
    );
    assert_eq!(out.details.token_count, Some(4242));

    // Routed the conversation id and the agent's isolated run dir (not the root).
    let (conv, cwd) = seen.lock().unwrap().clone().expect("inspector was called");
    assert_eq!(conv, "conv-123");
    assert_eq!(cwd, format!("/home/me/proj/.ideai/run/{aid}"));
}

// ---------------------------------------------------------------------------
// (b) inspector returns NotFound → empty details, no error
// ---------------------------------------------------------------------------

#[tokio::test]
async fn not_found_degrades_to_empty_details() {
    let pid = ProfileId::from_uuid(Uuid::from_u128(7));
    let aid = AgentId::from_uuid(Uuid::from_u128(42));
    let a = agent(aid, pid);

    let (inspector, _seen) = FakeInspector::new(true, InspectResult::Err(InspectError::NotFound));

    let uc = InspectConversation::new(
        Arc::new(FakeContexts::with_agent(&a)),
        Arc::new(FakeProfiles(Arc::new(vec![profile(pid)]))),
        vec![inspector],
    );

    let out = uc.execute(input(aid)).await.unwrap();
    assert_eq!(out.details.last_topic, None);
    assert_eq!(out.details.token_count, None);
}

// ---------------------------------------------------------------------------
// (b') inspector returns Read error → empty details, no error
// ---------------------------------------------------------------------------

#[tokio::test]
async fn read_error_degrades_to_empty_details() {
    let pid = ProfileId::from_uuid(Uuid::from_u128(7));
    let aid = AgentId::from_uuid(Uuid::from_u128(42));
    let a = agent(aid, pid);

    let (inspector, _seen) = FakeInspector::new(
        true,
        InspectResult::Err(InspectError::Read("boom".to_owned())),
    );

    let uc = InspectConversation::new(
        Arc::new(FakeContexts::with_agent(&a)),
        Arc::new(FakeProfiles(Arc::new(vec![profile(pid)]))),
        vec![inspector],
    );

    let out = uc.execute(input(aid)).await.unwrap();
    assert_eq!(
        out.details,
        ConversationDetails {
            last_topic: None,
            token_count: None
        }
    );
}

// ---------------------------------------------------------------------------
// (c) empty Vec (no inspector) → empty details, no error
// ---------------------------------------------------------------------------

#[tokio::test]
async fn no_inspector_yields_empty_details() {
    let pid = ProfileId::from_uuid(Uuid::from_u128(7));
    let aid = AgentId::from_uuid(Uuid::from_u128(42));
    let a = agent(aid, pid);

    let uc = InspectConversation::new(
        Arc::new(FakeContexts::with_agent(&a)),
        Arc::new(FakeProfiles(Arc::new(vec![profile(pid)]))),
        Vec::new(),
    );

    let out = uc.execute(input(aid)).await.unwrap();
    assert_eq!(out.details.last_topic, None);
    assert_eq!(out.details.token_count, None);
}

// ---------------------------------------------------------------------------
// (c') an inspector that does NOT support the profile is skipped → empty
// ---------------------------------------------------------------------------

#[tokio::test]
async fn unsupported_inspector_is_skipped() {
    let pid = ProfileId::from_uuid(Uuid::from_u128(7));
    let aid = AgentId::from_uuid(Uuid::from_u128(42));
    let a = agent(aid, pid);

    let (inspector, seen) = FakeInspector::new(
        false, // does not support
        InspectResult::Ok(ConversationDetails {
            last_topic: Some("should never surface".to_owned()),
            token_count: Some(1),
        }),
    );

    let uc = InspectConversation::new(
        Arc::new(FakeContexts::with_agent(&a)),
        Arc::new(FakeProfiles(Arc::new(vec![profile(pid)]))),
        vec![inspector],
    );

    let out = uc.execute(input(aid)).await.unwrap();
    assert_eq!(out.details.last_topic, None);
    assert_eq!(out.details.token_count, None);
    // details() must not have been called.
    assert!(seen.lock().unwrap().is_none());
}

// ---------------------------------------------------------------------------
// Genuine resolution failures still error (unknown agent)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn unknown_agent_errors() {
    let aid = AgentId::from_uuid(Uuid::from_u128(99));
    let (inspector, _seen) = FakeInspector::new(
        true,
        InspectResult::Ok(ConversationDetails {
            last_topic: None,
            token_count: None,
        }),
    );

    let uc = InspectConversation::new(
        Arc::new(FakeContexts::empty()),
        Arc::new(FakeProfiles(Arc::new(Vec::new()))),
        vec![inspector],
    );

    let err = uc.execute(input(aid)).await.unwrap_err();
    // Should be a NOT_FOUND-class error, not a panic / empty success.
    assert_eq!(err.code(), "NOT_FOUND");
}
