import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { prisma, resetDb, seedTestOrganization, testSecretEnvelope, DEFAULT_ORG_ID } from "./helpers.js";
import { createPdfToMdBundleAdapter, CapabilityPathEscape } from "../../src/capability/pdfToMdBundle.js";
import { DocmindClientError, type CapabilityProviderClient, type DocmindParseResult } from "../../src/capability/docmindClient.js";
import type { CapabilitySecretPayload } from "../../src/capability/types.js";

const CAPABILITY_ID = "pdf_to_md_bundle";
const PROVIDER_ID = "aliyun_docmind";

/**
 * ADR-0027: pdf_to_md_bundle adapter contract tests. Proves:
 *   - workspace containment (input + output confined to run workspace)
 *   - fail-closed credential resolution (no ACTIVE connection → error)
 *   - UsageFact attribution with non-token meter (pages), costSource rules
 *   - outputs (md + images) written into workspace
 *   - docmind client errors propagate
 * The client is mocked; the prisma + envelope + filesystem are real.
 */
describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
  let workspaceRoot: string;
  let runId: string;

  beforeEach(async () => {
    await resetDb();
    await seedTestOrganization();
    workspaceRoot = await mkdtemp(join(tmpdir(), "cph-cap-"));
    runId = "run-cap-test";
    // AgentRun is required for the UsageFact FK.
    await prisma.project.create({
      data: {
        id: "proj-cap",
        organizationId: DEFAULT_ORG_ID,
        name: "Cap Test",
        workspaceDir: workspaceRoot,
      },
    });
    await prisma.agentRun.create({
      data: {
        id: runId,
        projectId: "proj-cap",
        entrypoint: "FEISHU",
        provider: "openrouter",
        model: "mock-model",
        status: "ACTIVE",
        prompt: "convert this pdf",
        metadata: {},
      },
    });
  });

  afterEach(async () => {
    await rm(workspaceRoot, { recursive: true, force: true }).catch(() => {});
  });

  async function seedActiveCapabilityConnection(): Promise<void> {
    const payload: CapabilitySecretPayload = {
      schemaVersion: 1,
      accessKeyId: "LTAI-test-key-id",
      accessKeySecret: "test-secret-never-log",
      endpoint: "docmind-api.cn-hangzhou.aliyuncs.com",
    };
    const connection = await prisma.organizationCapabilityConnection.create({
      data: {
        id: "cap-conn-1",
        organizationId: DEFAULT_ORG_ID,
        capabilityId: CAPABILITY_ID,
        status: "ACTIVE",
        activatedAt: new Date(),
      },
    });
    const envelope = testSecretEnvelope.encryptJson(
      {
        purpose: "capability",
        organizationId: DEFAULT_ORG_ID,
        connectionId: connection.id,
        secretVersionId: "cap-sv-1",
      },
      payload,
    );
    await prisma.capabilityCredentialVersion.create({
      data: {
        id: "cap-sv-1",
        connectionId: connection.id,
        version: 1,
        envelope: envelope as object,
        keyId: envelope.keyId,
      },
    });
    await prisma.organizationCapabilityConnection.update({
      where: { id: connection.id },
      data: { activeSecretVersionId: "cap-sv-1" },
    });
  }

  function mockDocmindClient(result: Partial<DocmindParseResult> = {}): CapabilityProviderClient {
    const full: DocmindParseResult = {
      markdown: "# Parsed Document\n\nHello world.\n\n![fig](page_1.jpg)\n",
      images: [
        { filename: "page_1.jpg", data: new Uint8Array([0xff, 0xd8, 0xff, 0xe0]) },
      ],
      pageCount: 3,
      costUsd: 0.0168,
      requestId: "docmind-job-abc",
      ...result,
    };
    return {
      parse: vi.fn(async (): Promise<DocmindParseResult> => full),
    };
  }

  function makeAdapter(client: CapabilityProviderClient) {
    return createPdfToMdBundleAdapter({
      secrets: testSecretEnvelope,
      client,
      prisma,
    });
  }

  async function seedInputPdf(name: string = "input.pdf"): Promise<string> {
    const dir = join(workspaceRoot, "sources");
    await mkdir(dir, { recursive: true });
    const path = join(dir, name);
    await writeFile(path, "%PDF-1.4 fake pdf bytes");
    return join("sources", name);
  }

  it("writes md + images into the workspace and records a UsageFact", async () => {
    await seedActiveCapabilityConnection();
    const inputPath = await seedInputPdf();
    const client = mockDocmindClient();
    const adapter = makeAdapter(client);

    const result = await adapter.invoke({
      runId,
      organizationId: DEFAULT_ORG_ID,
      projectId: "proj-cap",
      workspaceDir: workspaceRoot,
      inputPath,
      outputDir: "output",
      prisma,
    });

    // Outputs landed in workspace.
    const md = await readFile(join(workspaceRoot, "output", "document.md"), "utf8");
    expect(md).toContain("# Parsed Document");
    expect(result.artifacts.some((a) => a.kind === "markdown" && a.path === "output/document.md")).toBe(true);
    expect(result.artifacts.some((a) => a.kind === "image" && a.path === "output/page_1.jpg")).toBe(true);

    // Client received the decrypted credential, not the env.
    const call = (client.parse as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as CapabilitySecretPayload;
    expect(call.accessKeyId).toBe("LTAI-test-key-id");
    expect(call.endpoint).toBe("docmind-api.cn-hangzhou.aliyuncs.com");

    // UsageFact written with non-token meter and provider_reported cost.
    const facts = await prisma.usageFact.findMany({ where: { runId } });
    expect(facts).toHaveLength(1);
    const fact = facts[0]!;
    expect(fact.kind).toBe("external_capability");
    expect(fact.provider).toBe(PROVIDER_ID);
    expect(fact.capabilityId).toBe(CAPABILITY_ID);
    expect(Number(fact.quantity)).toBe(3);
    expect(fact.unit).toBe("pages");
    expect(Number(fact.costUsd!)).toBeCloseTo(0.0168, 8);
    expect(fact.costSource).toBe("provider_reported");
    expect(fact.correlationId).toBe("docmind-job-abc");
  });

  it("writes UsageFact with costSource=unknown when docmind reports no cost", async () => {
    await seedActiveCapabilityConnection();
    const inputPath = await seedInputPdf();
    const client = mockDocmindClient({ costUsd: null, requestId: null });
    const adapter = makeAdapter(client);

    await adapter.invoke({
      runId,
      organizationId: DEFAULT_ORG_ID,
      projectId: "proj-cap",
      workspaceDir: workspaceRoot,
      inputPath,
      outputDir: "output",
      prisma,
    });

    const fact = (await prisma.usageFact.findFirstOrThrow({ where: { runId } }));
    expect(fact.costUsd).toBeNull();
    expect(fact.costSource).toBe("unknown");
    // quantity still recorded — missing cost ≠ zero consumption (ADR-0022).
    expect(Number(fact.quantity)).toBe(3);
  });

  it("fails closed when the org has no ACTIVE capability connection", async () => {
    // No connection seeded.
    const inputPath = await seedInputPdf();
    const adapter = makeAdapter(mockDocmindClient());

    await expect(adapter.invoke({
      runId,
      organizationId: DEFAULT_ORG_ID,
      projectId: "proj-cap",
      workspaceDir: workspaceRoot,
      inputPath,
      outputDir: "output",
      prisma,
    })).rejects.toThrow(/no ACTIVE capability connection/);

    // No UsageFact written for a failed resolution.
    const facts = await prisma.usageFact.findMany({ where: { runId } });
    expect(facts).toHaveLength(0);
  });

  it("rejects an input path that escapes the workspace", async () => {
    await seedActiveCapabilityConnection();
    const adapter = makeAdapter(mockDocmindClient());

    await expect(adapter.invoke({
      runId,
      organizationId: DEFAULT_ORG_ID,
      projectId: "proj-cap",
      workspaceDir: workspaceRoot,
      inputPath: "../../../etc/passwd",
      outputDir: "output",
      prisma,
    })).rejects.toBeInstanceOf(CapabilityPathEscape);
  });

  it("rejects an output path that escapes the workspace", async () => {
    await seedActiveCapabilityConnection();
    const inputPath = await seedInputPdf();
    const adapter = makeAdapter(mockDocmindClient());

    await expect(adapter.invoke({
      runId,
      organizationId: DEFAULT_ORG_ID,
      projectId: "proj-cap",
      workspaceDir: workspaceRoot,
      inputPath,
      outputDir: "../../outside",
      prisma,
    })).rejects.toBeInstanceOf(CapabilityPathEscape);
  });

  it("propagates docmind client errors without writing a UsageFact", async () => {
    await seedActiveCapabilityConnection();
    const inputPath = await seedInputPdf();
    const client: CapabilityProviderClient = {
      parse: vi.fn(async (): Promise<DocmindParseResult> => {
        throw new DocmindClientError("upstream 502", "docmind_rejected", 502);
      }),
    };
    const adapter = makeAdapter(client);

    await expect(adapter.invoke({
      runId,
      organizationId: DEFAULT_ORG_ID,
      projectId: "proj-cap",
      workspaceDir: workspaceRoot,
      inputPath,
      outputDir: "output",
      prisma,
    })).rejects.toThrow(/upstream 502/);

    const facts = await prisma.usageFact.findMany({ where: { runId } });
    expect(facts).toHaveLength(0);
  });

  it("rejects empty markdown output as a parse failure", async () => {
    await seedActiveCapabilityConnection();
    const inputPath = await seedInputPdf();
    const client = mockDocmindClient({ markdown: "" });
    const adapter = makeAdapter(client);

    await expect(adapter.invoke({
      runId,
      organizationId: DEFAULT_ORG_ID,
      projectId: "proj-cap",
      workspaceDir: workspaceRoot,
      inputPath,
      outputDir: "output",
      prisma,
    })).rejects.toThrow(/empty markdown/);
  });
});
