use std::path::PathBuf;
use std::sync::Arc;

use application::McpRuntime;
use async_trait::async_trait;
use domain::permission::{EffectivePermissions, PermissionSet, Posture};
use domain::ports::{SessionPlan, StructuredSessionEnvironmentPreparer};
use domain::profile::{
    McpCapability, McpConfigStrategy, McpTransport, OpenCodeConfig, StructuredAdapter,
};
use domain::{
    AgentProfile, AgentRuntime, AssistantContextProvider, ContextInjection, ContextInjectionPlan,
    FileSystem, Issue, IssueActor, IssueId, IssueNumber, IssuePriority, IssueStatus, MarkdownDoc,
    PreparedContext, ProfileId, Project, ProjectId, ProjectPath, RemotePath, RuntimeError,
    SpawnSpec,
};
use infrastructure::{
    FsAssistantContextStore, FsSecretStore, LocalFileSystem, TicketAssistantEnvironmentPreparer,
};
use uuid::Uuid;

struct TempDir(PathBuf);

impl TempDir {
    fn new() -> Self {
        let path = std::env::temp_dir().join(format!("idea-assistant-context-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&path).unwrap();
        Self(path)
    }

    fn app_data_dir(&self) -> String {
        self.0.join("app-data").to_string_lossy().into_owned()
    }

    fn project_root(&self) -> ProjectPath {
        ProjectPath::new(self.0.join("project").to_string_lossy().into_owned()).unwrap()
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.0);
    }
}

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

fn issue(number: u64) -> Issue {
    Issue::new(
        IssueId::from_uuid(Uuid::from_u128(number as u128)),
        IssueNumber::new(number).unwrap(),
        "Clarify onboarding",
        MarkdownDoc::new("Initial description body"),
        IssueStatus::Open,
        IssuePriority::High,
        MarkdownDoc::new("Carnet notes"),
        Vec::new(),
        Vec::new(),
        IssueActor::User,
        1_000,
    )
    .unwrap()
}

fn store(app_data_dir: String) -> FsAssistantContextStore {
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    FsAssistantContextStore::new(fs, app_data_dir)
}

struct FakeRuntime;

#[async_trait]
impl AgentRuntime for FakeRuntime {
    async fn detect(&self, _profile: &AgentProfile) -> Result<bool, RuntimeError> {
        Ok(true)
    }

    fn prepare_invocation(
        &self,
        _profile: &AgentProfile,
        _ctx: &PreparedContext,
        cwd: &ProjectPath,
        _session: &SessionPlan,
    ) -> Result<SpawnSpec, RuntimeError> {
        Ok(SpawnSpec {
            command: "assistant-cli".to_owned(),
            args: Vec::new(),
            cwd: cwd.clone(),
            env: Vec::new(),
            context_plan: Some(ContextInjectionPlan::File {
                // Deliberately looks like the real ticket carnet path. The preparer
                // must still materialise it under the isolated assistant cwd, never
                // under the project root.
                target: ".ideai/tickets/7/carnet.md".to_owned(),
            }),
            sandbox: None,
        })
    }
}

fn mcp_profile() -> AgentProfile {
    AgentProfile::new(
        ProfileId::from_uuid(Uuid::from_u128(9)),
        "Claude",
        "claude",
        Vec::new(),
        ContextInjection::stdin(),
        None,
        "{projectRoot}",
        None,
    )
    .unwrap()
    .with_mcp(McpCapability::new(
        McpConfigStrategy::config_file(".mcp.json").unwrap(),
        McpTransport::Stdio,
    ))
}

fn opencode_profile() -> AgentProfile {
    AgentProfile::new(
        ProfileId::from_uuid(Uuid::from_u128(10)),
        "OpenCode",
        "opencode",
        Vec::new(),
        ContextInjection::stdin(),
        None,
        "{projectRoot}",
        None,
    )
    .unwrap()
    .with_structured_adapter(StructuredAdapter::OpenCode)
    .with_opencode(
        OpenCodeConfig::new("http://localhost:8080/v1", None, "qwen3-coder", None, None).unwrap(),
    )
    .with_mcp(McpCapability::new(
        McpConfigStrategy::open_code_config("opencode.json").unwrap(),
        McpTransport::Stdio,
    ))
}

fn effective(fallback: Posture) -> EffectivePermissions {
    domain::permission::resolve(Some(&PermissionSet::new(Vec::new(), fallback)), None).unwrap()
}

#[tokio::test]
async fn default_context_is_embedded_and_injects_the_ticket() {
    let tmp = TempDir::new();
    let project = project(tmp.project_root());
    let store = store(tmp.app_data_dir());

    let ctx = store
        .prepare_ticket_assistant_context(&project, &issue(7))
        .await
        .unwrap();

    let body = ctx.content.as_str();
    assert!(body.contains("# Ticket Assistant"));
    assert!(body.contains("- Ref: #7"));
    assert!(body.contains("- Title: Clarify onboarding"));
    assert!(body.contains("Initial description body"));
    assert!(body.contains("Carnet notes"));
    assert_eq!(ctx.project_root, project.root.as_str());
    assert_eq!(ctx.relative_path, "ticket-assistant.md");
}

#[tokio::test]
async fn app_data_override_replaces_the_embedded_default() {
    let tmp = TempDir::new();
    let app_data_dir = tmp.app_data_dir();
    let override_path = PathBuf::from(&app_data_dir)
        .join("assistant")
        .join("ticket-assistant.md");
    std::fs::create_dir_all(override_path.parent().unwrap()).unwrap();
    std::fs::write(&override_path, "# Custom assistant").unwrap();

    let project = project(tmp.project_root());
    let store = store(app_data_dir);
    let ctx = store
        .prepare_ticket_assistant_context(&project, &issue(8))
        .await
        .unwrap();

    let body = ctx.content.as_str();
    assert!(body.starts_with("# Custom assistant"));
    assert!(!body.contains("You are IdeA's ticket editing assistant"));
    assert!(body.contains("- Ref: #8"));
}

#[tokio::test]
async fn environment_preparer_materialises_context_and_mcp_under_isolated_app_data() {
    let tmp = TempDir::new();
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    let app_data_dir = tmp.app_data_dir();
    let project = project(tmp.project_root());
    let project_carnet = PathBuf::from(project.root.as_str())
        .join(".ideai")
        .join("tickets")
        .join("7")
        .join("carnet.md");
    let prepared = PreparedContext {
        content: MarkdownDoc::new("assistant-only context"),
        relative_path: "ticket-assistant.md".to_owned(),
        project_root: project.root.as_str().to_owned(),
    };
    let requester = "ticket-assistant:00000000000000000000000000000001:7".to_owned();
    let preparer = TicketAssistantEnvironmentPreparer::new(
        fs.clone(),
        app_data_dir.clone(),
        Arc::new(FakeRuntime),
        Arc::new(move |_, _| {
            Some(McpRuntime {
                exe: "/opt/idea/idea".to_owned(),
                endpoint: "127.0.0.1:4567".to_owned(),
                project_id: "00000000000000000000000000000001".to_owned(),
                requester: requester.clone(),
            })
        }),
        Arc::new(FsSecretStore::new(fs.clone(), app_data_dir.clone())),
    );

    let env = preparer
        .prepare_ticket_assistant(
            &project,
            issue(7).reference(),
            &mcp_profile(),
            &prepared,
            "ticket-assistant:00000000000000000000000000000001:7",
            None,
        )
        .await
        .unwrap();

    let run_dir = PathBuf::from(&app_data_dir)
        .join("assistant")
        .join("tickets")
        .join("00000000000000000000000000000001")
        .join("7");
    assert_eq!(env.cwd.as_str(), run_dir.to_string_lossy());

    let isolated_carnet = run_dir
        .join(".ideai")
        .join("tickets")
        .join("7")
        .join("carnet.md");
    let isolated_mcp = run_dir.join(".mcp.json");
    assert_eq!(
        fs.read(&RemotePath::new(
            isolated_carnet.to_string_lossy().into_owned()
        ))
        .await
        .unwrap(),
        b"assistant-only context"
    );
    let mcp = String::from_utf8(
        fs.read(&RemotePath::new(
            isolated_mcp.to_string_lossy().into_owned(),
        ))
        .await
        .unwrap(),
    )
    .unwrap();
    assert!(mcp.contains(r#""idea""#));
    assert!(mcp.contains(r#""--requester""#));
    assert!(mcp.contains("ticket-assistant:00000000000000000000000000000001:7"));
    assert!(
        !project_carnet.exists(),
        "assistant context must not be written into the real project ticket carnet"
    );
}

#[tokio::test]
async fn opencode_ticket_assistant_projects_project_permissions_into_isolated_config() {
    let tmp = TempDir::new();
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    let app_data_dir = tmp.app_data_dir();
    let project = project(tmp.project_root());
    let prepared = PreparedContext {
        content: MarkdownDoc::new("assistant-only context"),
        relative_path: "ticket-assistant.md".to_owned(),
        project_root: project.root.as_str().to_owned(),
    };
    let requester = "ticket-assistant:00000000000000000000000000000001:7".to_owned();
    let preparer = TicketAssistantEnvironmentPreparer::new(
        fs.clone(),
        app_data_dir,
        Arc::new(FakeRuntime),
        Arc::new(move |_, _| {
            Some(McpRuntime {
                exe: "/opt/idea/idea".to_owned(),
                endpoint: "127.0.0.1:4567".to_owned(),
                project_id: "00000000000000000000000000000001".to_owned(),
                requester: requester.clone(),
            })
        }),
        Arc::new(FsSecretStore::new(fs.clone(), tmp.app_data_dir())),
    );

    let env = preparer
        .prepare_ticket_assistant(
            &project,
            issue(7).reference(),
            &opencode_profile(),
            &prepared,
            "ticket-assistant:00000000000000000000000000000001:7",
            Some(&effective(Posture::Deny)),
        )
        .await
        .unwrap();

    let config_path = PathBuf::from(env.cwd.as_str()).join("opencode.json");
    let config: serde_json::Value = serde_json::from_slice(
        &fs.read(&RemotePath::new(config_path.to_string_lossy().into_owned()))
            .await
            .unwrap(),
    )
    .unwrap();
    assert_eq!(config["permission"]["bash"], "deny");
    assert_eq!(config["permission"]["edit"], "deny");
}

#[tokio::test]
async fn claude_ticket_assistant_does_not_project_effective_permissions() {
    let tmp = TempDir::new();
    let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
    let app_data_dir = tmp.app_data_dir();
    let project = project(tmp.project_root());
    let prepared = PreparedContext {
        content: MarkdownDoc::new("assistant-only context"),
        relative_path: "ticket-assistant.md".to_owned(),
        project_root: project.root.as_str().to_owned(),
    };
    let requester = "ticket-assistant:00000000000000000000000000000001:7".to_owned();
    let preparer = TicketAssistantEnvironmentPreparer::new(
        fs.clone(),
        app_data_dir,
        Arc::new(FakeRuntime),
        Arc::new(move |_, _| {
            Some(McpRuntime {
                exe: "/opt/idea/idea".to_owned(),
                endpoint: "127.0.0.1:4567".to_owned(),
                project_id: "00000000000000000000000000000001".to_owned(),
                requester: requester.clone(),
            })
        }),
        Arc::new(FsSecretStore::new(fs.clone(), tmp.app_data_dir())),
    );

    let env = preparer
        .prepare_ticket_assistant(
            &project,
            issue(7).reference(),
            &mcp_profile(),
            &prepared,
            "ticket-assistant:00000000000000000000000000000001:7",
            Some(&effective(Posture::Deny)),
        )
        .await
        .unwrap();

    let mcp_path = PathBuf::from(env.cwd.as_str()).join(".mcp.json");
    let mcp: serde_json::Value = serde_json::from_slice(
        &fs.read(&RemotePath::new(mcp_path.to_string_lossy().into_owned()))
            .await
            .unwrap(),
    )
    .unwrap();
    assert!(mcp.get("permission").is_none());
}
