import { randomUUID } from "node:crypto";
function normalizeScope(input) {
    return {
        scopeKind: input.scopeKind,
        scopeId: input.scopeId,
        namespace: input.namespace ?? "default",
        stateKey: input.stateKey,
    };
}
function stateMapKey(input) {
    const normalized = normalizeScope(input);
    return `${normalized.scopeKind}|${normalized.scopeId ?? ""}|${normalized.namespace}|${normalized.stateKey}`;
}
function allowsEvent(filter, event) {
    if (!filter)
        return true;
    if (filter.companyId && filter.companyId !== String(event.payload?.companyId ?? ""))
        return false;
    if (filter.projectId && filter.projectId !== String(event.payload?.projectId ?? ""))
        return false;
    if (filter.agentId && filter.agentId !== String(event.payload?.agentId ?? ""))
        return false;
    return true;
}
function requireCapability(manifest, allowed, capability) {
    if (allowed.has(capability))
        return;
    throw new Error(`Plugin '${manifest.id}' is missing required capability '${capability}' in test harness`);
}
function requireCompanyId(companyId) {
    if (!companyId)
        throw new Error("companyId is required for this operation");
    return companyId;
}
function isInCompany(record, companyId) {
    return Boolean(record && record.companyId === companyId);
}
/**
 * Create an in-memory host harness for plugin worker tests.
 *
 * The harness enforces declared capabilities and simulates host APIs, so tests
 * can validate plugin behavior without spinning up the Paperclip server runtime.
 */
export function createTestHarness(options) {
    const manifest = options.manifest;
    const capabilitySet = new Set(options.capabilities ?? manifest.capabilities);
    let currentConfig = { ...(options.config ?? {}) };
    const logs = [];
    const activity = [];
    const metrics = [];
    const state = new Map();
    const entities = new Map();
    const entityExternalIndex = new Map();
    const companies = new Map();
    const projects = new Map();
    const issues = new Map();
    const issueComments = new Map();
    const agents = new Map();
    const goals = new Map();
    const projectWorkspaces = new Map();
    const sessions = new Map();
    const sessionEventCallbacks = new Map();
    const events = [];
    const jobs = new Map();
    const launchers = new Map();
    const dataHandlers = new Map();
    const actionHandlers = new Map();
    const toolHandlers = new Map();
    const ctx = {
        manifest,
        config: {
            async get() {
                return { ...currentConfig };
            },
        },
        events: {
            on(name, filterOrFn, maybeFn) {
                requireCapability(manifest, capabilitySet, "events.subscribe");
                let registration;
                if (typeof filterOrFn === "function") {
                    registration = { name, fn: filterOrFn };
                }
                else {
                    if (!maybeFn)
                        throw new Error("event handler is required");
                    registration = { name, filter: filterOrFn, fn: maybeFn };
                }
                events.push(registration);
                return () => {
                    const idx = events.indexOf(registration);
                    if (idx !== -1)
                        events.splice(idx, 1);
                };
            },
            async emit(name, companyId, payload) {
                requireCapability(manifest, capabilitySet, "events.emit");
                await harness.emit(`plugin.${manifest.id}.${name}`, payload, { companyId });
            },
        },
        jobs: {
            register(key, fn) {
                requireCapability(manifest, capabilitySet, "jobs.schedule");
                jobs.set(key, fn);
            },
        },
        launchers: {
            register(launcher) {
                launchers.set(launcher.id, launcher);
            },
        },
        http: {
            async fetch(url, init) {
                requireCapability(manifest, capabilitySet, "http.outbound");
                return fetch(url, init);
            },
        },
        secrets: {
            async resolve(secretRef) {
                requireCapability(manifest, capabilitySet, "secrets.read-ref");
                return `resolved:${secretRef}`;
            },
        },
        activity: {
            async log(entry) {
                requireCapability(manifest, capabilitySet, "activity.log.write");
                activity.push(entry);
            },
        },
        state: {
            async get(input) {
                requireCapability(manifest, capabilitySet, "plugin.state.read");
                return state.has(stateMapKey(input)) ? state.get(stateMapKey(input)) : null;
            },
            async set(input, value) {
                requireCapability(manifest, capabilitySet, "plugin.state.write");
                state.set(stateMapKey(input), value);
            },
            async delete(input) {
                requireCapability(manifest, capabilitySet, "plugin.state.write");
                state.delete(stateMapKey(input));
            },
        },
        entities: {
            async upsert(input) {
                const externalKey = input.externalId
                    ? `${input.entityType}|${input.scopeKind}|${input.scopeId ?? ""}|${input.externalId}`
                    : null;
                const existingId = externalKey ? entityExternalIndex.get(externalKey) : undefined;
                const existing = existingId ? entities.get(existingId) : undefined;
                const now = new Date().toISOString();
                const previousExternalKey = existing?.externalId
                    ? `${existing.entityType}|${existing.scopeKind}|${existing.scopeId ?? ""}|${existing.externalId}`
                    : null;
                const record = existing
                    ? {
                        ...existing,
                        entityType: input.entityType,
                        scopeKind: input.scopeKind,
                        scopeId: input.scopeId ?? null,
                        externalId: input.externalId ?? null,
                        title: input.title ?? null,
                        status: input.status ?? null,
                        data: input.data,
                        updatedAt: now,
                    }
                    : {
                        id: randomUUID(),
                        entityType: input.entityType,
                        scopeKind: input.scopeKind,
                        scopeId: input.scopeId ?? null,
                        externalId: input.externalId ?? null,
                        title: input.title ?? null,
                        status: input.status ?? null,
                        data: input.data,
                        createdAt: now,
                        updatedAt: now,
                    };
                entities.set(record.id, record);
                if (previousExternalKey && previousExternalKey !== externalKey) {
                    entityExternalIndex.delete(previousExternalKey);
                }
                if (externalKey)
                    entityExternalIndex.set(externalKey, record.id);
                return record;
            },
            async list(query) {
                let out = [...entities.values()];
                if (query.entityType)
                    out = out.filter((r) => r.entityType === query.entityType);
                if (query.scopeKind)
                    out = out.filter((r) => r.scopeKind === query.scopeKind);
                if (query.scopeId)
                    out = out.filter((r) => r.scopeId === query.scopeId);
                if (query.externalId)
                    out = out.filter((r) => r.externalId === query.externalId);
                if (query.offset)
                    out = out.slice(query.offset);
                if (query.limit)
                    out = out.slice(0, query.limit);
                return out;
            },
        },
        projects: {
            async list(input) {
                requireCapability(manifest, capabilitySet, "projects.read");
                const companyId = requireCompanyId(input?.companyId);
                let out = [...projects.values()];
                out = out.filter((project) => project.companyId === companyId);
                if (input?.offset)
                    out = out.slice(input.offset);
                if (input?.limit)
                    out = out.slice(0, input.limit);
                return out;
            },
            async get(projectId, companyId) {
                requireCapability(manifest, capabilitySet, "projects.read");
                const project = projects.get(projectId);
                return isInCompany(project, companyId) ? project : null;
            },
            async listWorkspaces(projectId, companyId) {
                requireCapability(manifest, capabilitySet, "project.workspaces.read");
                if (!isInCompany(projects.get(projectId), companyId))
                    return [];
                return projectWorkspaces.get(projectId) ?? [];
            },
            async getPrimaryWorkspace(projectId, companyId) {
                requireCapability(manifest, capabilitySet, "project.workspaces.read");
                if (!isInCompany(projects.get(projectId), companyId))
                    return null;
                const workspaces = projectWorkspaces.get(projectId) ?? [];
                return workspaces.find((workspace) => workspace.isPrimary) ?? null;
            },
            async getWorkspaceForIssue(issueId, companyId) {
                requireCapability(manifest, capabilitySet, "project.workspaces.read");
                const issue = issues.get(issueId);
                if (!isInCompany(issue, companyId))
                    return null;
                const projectId = issue?.projectId;
                if (!projectId)
                    return null;
                if (!isInCompany(projects.get(projectId), companyId))
                    return null;
                const workspaces = projectWorkspaces.get(projectId) ?? [];
                return workspaces.find((workspace) => workspace.isPrimary) ?? null;
            },
        },
        companies: {
            async list(input) {
                requireCapability(manifest, capabilitySet, "companies.read");
                let out = [...companies.values()];
                if (input?.offset)
                    out = out.slice(input.offset);
                if (input?.limit)
                    out = out.slice(0, input.limit);
                return out;
            },
            async get(companyId) {
                requireCapability(manifest, capabilitySet, "companies.read");
                return companies.get(companyId) ?? null;
            },
        },
        issues: {
            async list(input) {
                requireCapability(manifest, capabilitySet, "issues.read");
                const companyId = requireCompanyId(input?.companyId);
                let out = [...issues.values()];
                out = out.filter((issue) => issue.companyId === companyId);
                if (input?.projectId)
                    out = out.filter((issue) => issue.projectId === input.projectId);
                if (input?.assigneeAgentId)
                    out = out.filter((issue) => issue.assigneeAgentId === input.assigneeAgentId);
                if (input?.status)
                    out = out.filter((issue) => issue.status === input.status);
                if (input?.offset)
                    out = out.slice(input.offset);
                if (input?.limit)
                    out = out.slice(0, input.limit);
                return out;
            },
            async get(issueId, companyId) {
                requireCapability(manifest, capabilitySet, "issues.read");
                const issue = issues.get(issueId);
                return isInCompany(issue, companyId) ? issue : null;
            },
            async create(input) {
                requireCapability(manifest, capabilitySet, "issues.create");
                const now = new Date();
                const record = {
                    id: randomUUID(),
                    companyId: input.companyId,
                    projectId: input.projectId ?? null,
                    projectWorkspaceId: null,
                    goalId: input.goalId ?? null,
                    parentId: input.parentId ?? null,
                    title: input.title,
                    description: input.description ?? null,
                    status: "todo",
                    priority: input.priority ?? "medium",
                    assigneeAgentId: input.assigneeAgentId ?? null,
                    assigneeUserId: null,
                    checkoutRunId: null,
                    executionRunId: null,
                    executionAgentNameKey: null,
                    executionLockedAt: null,
                    createdByAgentId: null,
                    createdByUserId: null,
                    issueNumber: null,
                    identifier: null,
                    requestDepth: 0,
                    billingCode: null,
                    assigneeAdapterOverrides: null,
                    executionWorkspaceId: null,
                    executionWorkspacePreference: null,
                    executionWorkspaceSettings: null,
                    startedAt: null,
                    completedAt: null,
                    cancelledAt: null,
                    hiddenAt: null,
                    createdAt: now,
                    updatedAt: now,
                };
                issues.set(record.id, record);
                return record;
            },
            async update(issueId, patch, companyId) {
                requireCapability(manifest, capabilitySet, "issues.update");
                const record = issues.get(issueId);
                if (!isInCompany(record, companyId))
                    throw new Error(`Issue not found: ${issueId}`);
                const updated = {
                    ...record,
                    ...patch,
                    updatedAt: new Date(),
                };
                issues.set(issueId, updated);
                return updated;
            },
            async listComments(issueId, companyId) {
                requireCapability(manifest, capabilitySet, "issue.comments.read");
                if (!isInCompany(issues.get(issueId), companyId))
                    return [];
                return issueComments.get(issueId) ?? [];
            },
            async createComment(issueId, body, companyId) {
                requireCapability(manifest, capabilitySet, "issue.comments.create");
                const parentIssue = issues.get(issueId);
                if (!isInCompany(parentIssue, companyId)) {
                    throw new Error(`Issue not found: ${issueId}`);
                }
                const now = new Date();
                const comment = {
                    id: randomUUID(),
                    companyId: parentIssue.companyId,
                    issueId,
                    authorAgentId: null,
                    authorUserId: null,
                    body,
                    createdAt: now,
                    updatedAt: now,
                };
                const current = issueComments.get(issueId) ?? [];
                current.push(comment);
                issueComments.set(issueId, current);
                return comment;
            },
            documents: {
                async list(issueId, companyId) {
                    requireCapability(manifest, capabilitySet, "issue.documents.read");
                    if (!isInCompany(issues.get(issueId), companyId))
                        return [];
                    return [];
                },
                async get(issueId, _key, companyId) {
                    requireCapability(manifest, capabilitySet, "issue.documents.read");
                    if (!isInCompany(issues.get(issueId), companyId))
                        return null;
                    return null;
                },
                async upsert(input) {
                    requireCapability(manifest, capabilitySet, "issue.documents.write");
                    const parentIssue = issues.get(input.issueId);
                    if (!isInCompany(parentIssue, input.companyId)) {
                        throw new Error(`Issue not found: ${input.issueId}`);
                    }
                    throw new Error("documents.upsert is not implemented in test context");
                },
                async delete(issueId, _key, companyId) {
                    requireCapability(manifest, capabilitySet, "issue.documents.write");
                    const parentIssue = issues.get(issueId);
                    if (!isInCompany(parentIssue, companyId)) {
                        throw new Error(`Issue not found: ${issueId}`);
                    }
                },
            },
        },
        agents: {
            async list(input) {
                requireCapability(manifest, capabilitySet, "agents.read");
                const companyId = requireCompanyId(input?.companyId);
                let out = [...agents.values()];
                out = out.filter((agent) => agent.companyId === companyId);
                if (input?.status)
                    out = out.filter((agent) => agent.status === input.status);
                if (input?.offset)
                    out = out.slice(input.offset);
                if (input?.limit)
                    out = out.slice(0, input.limit);
                return out;
            },
            async get(agentId, companyId) {
                requireCapability(manifest, capabilitySet, "agents.read");
                const agent = agents.get(agentId);
                return isInCompany(agent, companyId) ? agent : null;
            },
            async pause(agentId, companyId) {
                requireCapability(manifest, capabilitySet, "agents.pause");
                const cid = requireCompanyId(companyId);
                const agent = agents.get(agentId);
                if (!isInCompany(agent, cid))
                    throw new Error(`Agent not found: ${agentId}`);
                if (agent.status === "terminated")
                    throw new Error("Cannot pause terminated agent");
                const updated = { ...agent, status: "paused", updatedAt: new Date() };
                agents.set(agentId, updated);
                return updated;
            },
            async resume(agentId, companyId) {
                requireCapability(manifest, capabilitySet, "agents.resume");
                const cid = requireCompanyId(companyId);
                const agent = agents.get(agentId);
                if (!isInCompany(agent, cid))
                    throw new Error(`Agent not found: ${agentId}`);
                if (agent.status === "terminated")
                    throw new Error("Cannot resume terminated agent");
                if (agent.status === "pending_approval")
                    throw new Error("Pending approval agents cannot be resumed");
                const updated = { ...agent, status: "idle", updatedAt: new Date() };
                agents.set(agentId, updated);
                return updated;
            },
            async invoke(agentId, companyId, opts) {
                requireCapability(manifest, capabilitySet, "agents.invoke");
                const cid = requireCompanyId(companyId);
                const agent = agents.get(agentId);
                if (!isInCompany(agent, cid))
                    throw new Error(`Agent not found: ${agentId}`);
                if (agent.status === "paused" ||
                    agent.status === "terminated" ||
                    agent.status === "pending_approval") {
                    throw new Error(`Agent is not invokable in its current state: ${agent.status}`);
                }
                return { runId: randomUUID() };
            },
            sessions: {
                async create(agentId, companyId, opts) {
                    requireCapability(manifest, capabilitySet, "agent.sessions.create");
                    const cid = requireCompanyId(companyId);
                    const agent = agents.get(agentId);
                    if (!isInCompany(agent, cid))
                        throw new Error(`Agent not found: ${agentId}`);
                    const session = {
                        sessionId: randomUUID(),
                        agentId,
                        companyId: cid,
                        status: "active",
                        createdAt: new Date().toISOString(),
                    };
                    sessions.set(session.sessionId, session);
                    return session;
                },
                async list(agentId, companyId) {
                    requireCapability(manifest, capabilitySet, "agent.sessions.list");
                    const cid = requireCompanyId(companyId);
                    return [...sessions.values()].filter((s) => s.agentId === agentId && s.companyId === cid && s.status === "active");
                },
                async sendMessage(sessionId, companyId, opts) {
                    requireCapability(manifest, capabilitySet, "agent.sessions.send");
                    const session = sessions.get(sessionId);
                    if (!session || session.status !== "active")
                        throw new Error(`Session not found or closed: ${sessionId}`);
                    if (session.companyId !== companyId)
                        throw new Error(`Session not found: ${sessionId}`);
                    if (opts.onEvent) {
                        sessionEventCallbacks.set(sessionId, opts.onEvent);
                    }
                    return { runId: randomUUID() };
                },
                async close(sessionId, companyId) {
                    requireCapability(manifest, capabilitySet, "agent.sessions.close");
                    const session = sessions.get(sessionId);
                    if (!session)
                        throw new Error(`Session not found: ${sessionId}`);
                    if (session.companyId !== companyId)
                        throw new Error(`Session not found: ${sessionId}`);
                    session.status = "closed";
                    sessionEventCallbacks.delete(sessionId);
                },
            },
        },
        goals: {
            async list(input) {
                requireCapability(manifest, capabilitySet, "goals.read");
                const companyId = requireCompanyId(input?.companyId);
                let out = [...goals.values()];
                out = out.filter((goal) => goal.companyId === companyId);
                if (input?.level)
                    out = out.filter((goal) => goal.level === input.level);
                if (input?.status)
                    out = out.filter((goal) => goal.status === input.status);
                if (input?.offset)
                    out = out.slice(input.offset);
                if (input?.limit)
                    out = out.slice(0, input.limit);
                return out;
            },
            async get(goalId, companyId) {
                requireCapability(manifest, capabilitySet, "goals.read");
                const goal = goals.get(goalId);
                return isInCompany(goal, companyId) ? goal : null;
            },
            async create(input) {
                requireCapability(manifest, capabilitySet, "goals.create");
                const now = new Date();
                const record = {
                    id: randomUUID(),
                    companyId: input.companyId,
                    title: input.title,
                    description: input.description ?? null,
                    level: input.level ?? "task",
                    status: input.status ?? "planned",
                    parentId: input.parentId ?? null,
                    ownerAgentId: input.ownerAgentId ?? null,
                    createdAt: now,
                    updatedAt: now,
                };
                goals.set(record.id, record);
                return record;
            },
            async update(goalId, patch, companyId) {
                requireCapability(manifest, capabilitySet, "goals.update");
                const record = goals.get(goalId);
                if (!isInCompany(record, companyId))
                    throw new Error(`Goal not found: ${goalId}`);
                const updated = {
                    ...record,
                    ...patch,
                    updatedAt: new Date(),
                };
                goals.set(goalId, updated);
                return updated;
            },
        },
        data: {
            register(key, handler) {
                dataHandlers.set(key, handler);
            },
        },
        actions: {
            register(key, handler) {
                actionHandlers.set(key, handler);
            },
        },
        streams: (() => {
            const channelCompanyMap = new Map();
            return {
                open(channel, companyId) {
                    channelCompanyMap.set(channel, companyId);
                },
                emit(_channel, _event) {
                    // No-op in test harness — events are not forwarded
                },
                close(channel) {
                    channelCompanyMap.delete(channel);
                },
            };
        })(),
        tools: {
            register(name, _decl, fn) {
                requireCapability(manifest, capabilitySet, "agent.tools.register");
                toolHandlers.set(name, fn);
            },
        },
        metrics: {
            async write(name, value, tags) {
                requireCapability(manifest, capabilitySet, "metrics.write");
                metrics.push({ name, value, tags });
            },
        },
        logger: {
            info(message, meta) {
                logs.push({ level: "info", message, meta });
            },
            warn(message, meta) {
                logs.push({ level: "warn", message, meta });
            },
            error(message, meta) {
                logs.push({ level: "error", message, meta });
            },
            debug(message, meta) {
                logs.push({ level: "debug", message, meta });
            },
        },
    };
    const harness = {
        ctx,
        seed(input) {
            for (const row of input.companies ?? [])
                companies.set(row.id, row);
            for (const row of input.projects ?? [])
                projects.set(row.id, row);
            for (const row of input.issues ?? [])
                issues.set(row.id, row);
            for (const row of input.issueComments ?? []) {
                const list = issueComments.get(row.issueId) ?? [];
                list.push(row);
                issueComments.set(row.issueId, list);
            }
            for (const row of input.agents ?? [])
                agents.set(row.id, row);
            for (const row of input.goals ?? [])
                goals.set(row.id, row);
        },
        setConfig(config) {
            currentConfig = { ...config };
        },
        async emit(eventType, payload, base) {
            const event = {
                eventId: base?.eventId ?? randomUUID(),
                eventType,
                companyId: base?.companyId ?? "test-company",
                occurredAt: base?.occurredAt ?? new Date().toISOString(),
                actorId: base?.actorId,
                actorType: base?.actorType,
                entityId: base?.entityId,
                entityType: base?.entityType,
                payload,
            };
            for (const handler of events) {
                const exactMatch = handler.name === event.eventType;
                const wildcardPluginAll = handler.name === "plugin.*" && String(event.eventType).startsWith("plugin.");
                const wildcardPluginOne = String(handler.name).endsWith(".*")
                    && String(event.eventType).startsWith(String(handler.name).slice(0, -1));
                if (!exactMatch && !wildcardPluginAll && !wildcardPluginOne)
                    continue;
                if (!allowsEvent(handler.filter, event))
                    continue;
                await handler.fn(event);
            }
        },
        async runJob(jobKey, partial = {}) {
            const handler = jobs.get(jobKey);
            if (!handler)
                throw new Error(`No job handler registered for '${jobKey}'`);
            await handler({
                jobKey,
                runId: partial.runId ?? randomUUID(),
                trigger: partial.trigger ?? "manual",
                scheduledAt: partial.scheduledAt ?? new Date().toISOString(),
            });
        },
        async getData(key, params = {}) {
            const handler = dataHandlers.get(key);
            if (!handler)
                throw new Error(`No data handler registered for '${key}'`);
            return await handler(params);
        },
        async performAction(key, params = {}) {
            const handler = actionHandlers.get(key);
            if (!handler)
                throw new Error(`No action handler registered for '${key}'`);
            return await handler(params);
        },
        async executeTool(name, params, runCtx = {}) {
            const handler = toolHandlers.get(name);
            if (!handler)
                throw new Error(`No tool handler registered for '${name}'`);
            const ctxToPass = {
                agentId: runCtx.agentId ?? "agent-test",
                runId: runCtx.runId ?? randomUUID(),
                companyId: runCtx.companyId ?? "company-test",
                projectId: runCtx.projectId ?? "project-test",
            };
            return await handler(params, ctxToPass);
        },
        getState(input) {
            return state.get(stateMapKey(input));
        },
        simulateSessionEvent(sessionId, event) {
            const cb = sessionEventCallbacks.get(sessionId);
            if (!cb)
                throw new Error(`No active session event callback for session: ${sessionId}`);
            cb({ ...event, sessionId });
        },
        logs,
        activity,
        metrics,
    };
    return harness;
}
//# sourceMappingURL=testing.js.map