use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, Mutex};

use application::{
    CloseTicketAssistant, CloseTicketAssistantInput, OpenTicketAssistant, OpenTicketAssistantInput,
    StructuredSessions,
};
use async_trait::async_trait;
use domain::ports::{
    AgentSession, AgentSessionError, AgentSessionFactory, ReplyStream, RuntimeError, SessionPlan,
    StructuredSessionEnvironment, StructuredSessionEnvironmentPreparer,
};
use domain::profile::StructuredAdapter;
use domain::{
    AgentProfile, AgentToolPolicy, AgentToolPolicyStore, AssistantContextError,
    AssistantContextProvider, ContextInjection, DomainEvent, EventBus, EventStream, Issue,
    IssueActor, IssueCarnet, IssueId, IssueListFilter, IssueNumber, IssuePriority, IssueRef,
    IssueStatus, IssueStore, IssueStoreError, IssueVersion, MarkdownDoc, PreparedContext,
    ProfileId, ProfileStore, Project, ProjectId, ProjectPath, RemoteRef, SessionId, StoreError,
};
use uuid::Uuid;

fn issue_ref(raw: &str) -> IssueRef {
    IssueRef::from_str(raw).unwrap()
}

fn project() -> Project {
    Project::new(
        ProjectId::from_uuid(Uuid::from_u128(1)),
        "demo",
        ProjectPath::new("/tmp/project").unwrap(),
        RemoteRef::local(),
        1_000,
    )
    .unwrap()
}

fn profile(id: ProfileId) -> AgentProfile {
    AgentProfile::new(
        id,
        "Claude",
        "claude",
        Vec::new(),
        ContextInjection::stdin(),
        None,
        "{projectRoot}",
        None,
    )
    .unwrap()
    .with_structured_adapter(StructuredAdapter::Claude)
}

fn issue(number: u64) -> Issue {
    Issue::new(
        IssueId::from_uuid(Uuid::from_u128(number as u128)),
        IssueNumber::new(number).unwrap(),
        "Ticket title",
        MarkdownDoc::new("Ticket description"),
        IssueStatus::Open,
        IssuePriority::High,
        MarkdownDoc::new("Ticket carnet"),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1_000,
    )
    .unwrap()
}

struct FakeIssues {
    issue: Issue,
}

#[async_trait]
impl IssueStore for FakeIssues {
    async fn create(&self, _root: &ProjectPath, _issue: &Issue) -> Result<(), IssueStoreError> {
        unimplemented!()
    }

    async fn get_by_ref(
        &self,
        _root: &ProjectPath,
        issue_ref: IssueRef,
    ) -> Result<Issue, IssueStoreError> {
        if self.issue.reference() == issue_ref {
            Ok(self.issue.clone())
        } else {
            Err(IssueStoreError::NotFound)
        }
    }

    async fn list(
        &self,
        _root: &ProjectPath,
        _filter: IssueListFilter,
    ) -> Result<Vec<domain::IssueIndexEntry>, IssueStoreError> {
        Ok(Vec::new())
    }

    async fn update(
        &self,
        _root: &ProjectPath,
        _issue: &Issue,
        _expected_version: IssueVersion,
    ) -> Result<(), IssueStoreError> {
        unimplemented!()
    }

    async fn delete(
        &self,
        _root: &ProjectPath,
        _issue_ref: IssueRef,
    ) -> Result<(), IssueStoreError> {
        unimplemented!()
    }

    async fn read_carnet(
        &self,
        _root: &ProjectPath,
        _issue_ref: IssueRef,
    ) -> Result<IssueCarnet, IssueStoreError> {
        unimplemented!()
    }

    async fn write_carnet(
        &self,
        _root: &ProjectPath,
        _issue_ref: IssueRef,
        _carnet: MarkdownDoc,
        _actor: IssueActor,
        _now_ms: u64,
        _expected_version: IssueVersion,
    ) -> Result<IssueCarnet, IssueStoreError> {
        unimplemented!()
    }
}

struct FakeProfiles {
    profile: AgentProfile,
}

#[async_trait]
impl ProfileStore for FakeProfiles {
    async fn list(&self) -> Result<Vec<AgentProfile>, StoreError> {
        Ok(vec![self.profile.clone()])
    }

    async fn save(&self, _p: &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(())
    }
}

#[derive(Default)]
struct FakeAssistantContext {
    prepared: Mutex<Option<PreparedContext>>,
}

#[async_trait]
impl AssistantContextProvider for FakeAssistantContext {
    async fn prepare_ticket_assistant_context(
        &self,
        project: &Project,
        issue: &Issue,
    ) -> Result<PreparedContext, AssistantContextError> {
        let ctx = PreparedContext {
            content: MarkdownDoc::new(format!(
                "assistant\n{}\n{}\n{}",
                issue.reference(),
                issue.description.as_str(),
                issue.carnet.as_str()
            )),
            relative_path: "ticket-assistant.md".to_owned(),
            project_root: project.root.as_str().to_owned(),
        };
        *self.prepared.lock().unwrap() = Some(ctx.clone());
        Ok(ctx)
    }
}

#[derive(Default)]
struct FakeEnvironmentPreparer {
    calls: Mutex<Vec<(IssueRef, String, PreparedContext)>>,
}

#[async_trait]
impl StructuredSessionEnvironmentPreparer for FakeEnvironmentPreparer {
    async fn prepare_ticket_assistant(
        &self,
        _project: &Project,
        issue_ref: IssueRef,
        _profile: &AgentProfile,
        prepared: &PreparedContext,
        requester: &str,
    ) -> Result<StructuredSessionEnvironment, RuntimeError> {
        self.calls
            .lock()
            .unwrap()
            .push((issue_ref, requester.to_owned(), prepared.clone()));
        Ok(StructuredSessionEnvironment {
            cwd: ProjectPath::new("/tmp/app-data/assistant/tickets/1/7").unwrap(),
            env: vec![(
                "CODEX_HOME".to_owned(),
                "/tmp/app-data/assistant/tickets/1/7/.codex".to_owned(),
            )],
        })
    }
}

struct FakeSession {
    id: SessionId,
    shutdowns: Arc<Mutex<usize>>,
}

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

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

    async fn send(&self, _prompt: &str) -> Result<ReplyStream, AgentSessionError> {
        Ok(Box::new(std::iter::empty()))
    }

    async fn shutdown(&self) -> Result<(), AgentSessionError> {
        *self.shutdowns.lock().unwrap() += 1;
        Ok(())
    }
}

#[derive(Default)]
struct FakeFactory {
    starts: Mutex<
        Vec<(
            PreparedContext,
            ProjectPath,
            SessionPlan,
            Option<String>,
            Vec<(String, String)>,
            Option<domain::SandboxPlan>,
        )>,
    >,
    shutdowns: Arc<Mutex<usize>>,
}

#[async_trait]
impl AgentSessionFactory for FakeFactory {
    fn supports(&self, _profile: &AgentProfile) -> bool {
        true
    }

    async fn start(
        &self,
        _profile: &AgentProfile,
        ctx: &PreparedContext,
        cwd: &ProjectPath,
        session: &SessionPlan,
        requester: Option<&str>,
        env: &[(String, String)],
        sandbox: Option<&domain::SandboxPlan>,
    ) -> Result<Arc<dyn AgentSession>, AgentSessionError> {
        self.starts.lock().unwrap().push((
            ctx.clone(),
            cwd.clone(),
            session.clone(),
            requester.map(str::to_owned),
            env.to_vec(),
            sandbox.cloned(),
        ));
        Ok(Arc::new(FakeSession {
            id: SessionId::new_random(),
            shutdowns: Arc::clone(&self.shutdowns),
        }))
    }
}

#[derive(Default)]
struct FakePolicies(Mutex<HashMap<String, AgentToolPolicy>>);

impl AgentToolPolicyStore for FakePolicies {
    fn set_policy(&self, requester: String, policy: AgentToolPolicy) {
        self.0.lock().unwrap().insert(requester, policy);
    }

    fn get_policy(&self, requester: &str) -> Option<AgentToolPolicy> {
        self.0.lock().unwrap().get(requester).cloned()
    }

    fn clear_policy(&self, requester: &str) {
        self.0.lock().unwrap().remove(requester);
    }
}

#[derive(Default)]
struct SpyBus(Mutex<Vec<DomainEvent>>);

impl EventBus for SpyBus {
    fn publish(&self, event: DomainEvent) {
        self.0.lock().unwrap().push(event);
    }

    fn subscribe(&self) -> EventStream {
        Box::new(std::iter::empty())
    }
}

#[tokio::test]
async fn open_then_close_ticket_assistant_sets_policy_injects_context_and_emits_events() {
    let issue = issue(7);
    let profile_id = ProfileId::from_uuid(Uuid::from_u128(9));
    let structured = Arc::new(StructuredSessions::new());
    let policies = Arc::new(FakePolicies::default());
    let events = Arc::new(SpyBus::default());
    let context = Arc::new(FakeAssistantContext::default());
    let environment = Arc::new(FakeEnvironmentPreparer::default());
    let factory = Arc::new(FakeFactory::default());
    let open = OpenTicketAssistant::new(
        Arc::new(FakeIssues {
            issue: issue.clone(),
        }),
        Arc::new(FakeProfiles {
            profile: profile(profile_id),
        }),
        context.clone(),
        environment.clone(),
        factory.clone(),
        structured.clone(),
        policies.clone(),
        events.clone(),
    );
    let close = CloseTicketAssistant::new(structured.clone(), policies.clone(), events.clone());

    let output = open
        .execute(OpenTicketAssistantInput {
            project: project(),
            issue_ref: issue.reference(),
            profile_id,
        })
        .await
        .unwrap();

    assert!(structured.session(&output.session_id).is_some());
    assert_eq!(
        output.requester,
        "ticket-assistant:00000000000000000000000000000001:7"
    );
    let policy = policies.get_policy(&output.requester).expect("policy set");
    assert!(policy.permits("idea_ticket_read"));
    assert!(policy.permits_ticket_mutation("idea_ticket_update", issue.reference()));
    assert!(policy.permits_ticket_mutation("idea_ticket_update_status", issue.reference()));
    assert!(policy.permits_ticket_mutation("idea_ticket_update_priority", issue.reference()));
    assert!(policy.permits_ticket_mutation("idea_ticket_update_carnet", issue.reference()));
    assert!(policy.permits_ticket_mutation("idea_ticket_link", issue.reference()));
    assert!(policy.permits_ticket_mutation("idea_ticket_unlink", issue.reference()));
    assert!(!policy.permits("idea_ticket_create"));
    assert!(!policy.permits("idea_ticket_list"));
    assert!(!policy.permits("idea_ask_agent"));
    assert!(!policy.permits_ticket_mutation("idea_ticket_update", issue_ref("#8")));
    let prepared = context.prepared.lock().unwrap().clone().unwrap();
    assert!(prepared.content.as_str().contains("#7"));
    assert!(prepared.content.as_str().contains("Ticket description"));
    assert!(prepared.content.as_str().contains("Ticket carnet"));
    let environment_calls = environment.calls.lock().unwrap();
    assert_eq!(environment_calls.len(), 1);
    assert_eq!(environment_calls[0].0, issue.reference());
    assert_eq!(environment_calls[0].1, output.requester);
    drop(environment_calls);
    let starts = factory.starts.lock().unwrap();
    assert_eq!(starts[0].1.as_str(), "/tmp/app-data/assistant/tickets/1/7");
    assert!(matches!(starts[0].2, SessionPlan::None));
    assert_eq!(starts[0].3.as_deref(), Some(output.requester.as_str()));
    assert_eq!(
        starts[0].4,
        vec![(
            "CODEX_HOME".to_owned(),
            "/tmp/app-data/assistant/tickets/1/7/.codex".to_owned()
        )]
    );
    assert!(starts[0].5.is_none());
    drop(starts);

    close
        .execute(CloseTicketAssistantInput {
            issue_ref: issue.reference(),
        })
        .await
        .unwrap();

    assert!(structured.session(&output.session_id).is_none());
    assert!(policies.get_policy(&output.requester).is_none());
    assert_eq!(*factory.shutdowns.lock().unwrap(), 1);
    let events = events.0.lock().unwrap();
    assert!(matches!(
        events[0],
        DomainEvent::TicketAssistantOpened { issue_ref, .. } if issue_ref == issue.reference()
    ));
    assert!(matches!(
        events[1],
        DomainEvent::TicketAssistantClosed { issue_ref } if issue_ref == issue.reference()
    ));
}
