// SPEC: PJ2026-010404 项目管理 draft-2026-06-25-p0-project-management-mdtodo; draft-2026-06-26-p1-mdtodo-web-operable.
// Responsibility: Fake-server browser regression for project root and MDTODO pages.

import { expect, saveScreenshot, test } from "../fixtures/test";

test.describe("project management pages", () => {
  test("renders project root and launches Workbench through public DTOs", async ({
    page,
  }, testInfo) => {
    await page.goto("/projects");
    await expect(page.getByTestId("project-management-root")).toBeVisible();
    await expect(page.getByTestId("project-management-summary")).toContainText(
      "Projects",
    );
    await expect(page.getByTestId("project-list")).toContainText(
      "HWLAB MDTODO",
    );
    await page
      .getByRole("link", { name: /HWLAB MDTODO/u })
      .first()
      .click();
    await expect(page).toHaveURL(/\/projects\/project_hwlab_v03/u);
    await expect(page.locator(".project-inspector")).toContainText(
      "mdtodo-file-tree",
    );
    await expect(page.getByText("[redacted]")).toHaveCount(0);

    await page.getByTestId("open-mdtodo-page").click();
    await expect(page).toHaveURL(/\/projects\/mdtodo/u);
    await expect(page.getByTestId("project-management-mdtodo")).toBeVisible();
    await expect(page.getByTestId("mdtodo-toolbar")).toBeVisible();
    await expect(page.getByTestId("mdtodo-source-select")).toHaveValue(
      "hwlab-v03-mdtodo",
    );
    await expect(page.getByTestId("mdtodo-file-select")).toHaveValue(
      "file_plan",
    );
    await expect(page.getByTestId("mdtodo-file-select")).toContainText(
      "hwlab-v03-mdtodo-web-sample.md",
    );
    await expect(page.getByTestId("mdtodo-file-select")).toContainText(
      "20260609_频率判断_用户反馈.md",
    );
    await expect(page.getByTestId("mdtodo-file-select")).not.toContainText(
      "Task_Report",
    );
    const workspaceBottomGap = await page
      .getByTestId("project-management-mdtodo")
      .evaluate((shell) => {
        const workspace = shell.querySelector(".mdtodo-workspace");
        if (!workspace) return Number.POSITIVE_INFINITY;
        const shellRect = shell.getBoundingClientRect();
        const workspaceRect = workspace.getBoundingClientRect();
        return Math.abs(shellRect.bottom - workspaceRect.bottom);
      });
    expect(workspaceBottomGap).toBeLessThanOrEqual(2);
    const outlineRatio = await page.locator(".task-index").evaluate((panel) => {
      const workspace = panel.closest(".mdtodo-workspace");
      if (!workspace) return 0;
      return (
        panel.getBoundingClientRect().width /
        workspace.getBoundingClientRect().width
      );
    });
    expect(outlineRatio).toBeGreaterThan(0.24);
    expect(outlineRatio).toBeLessThan(0.36);
    await expect(page.getByTestId("mdtodo-task-tree")).toContainText(
      "实现项目根导航",
    );
    await expect(page.getByTestId("mdtodo-task-page-fact")).toContainText(
      "已载入",
    );
    await expect(page.locator('[data-task-id="R1"]')).toBeVisible();
    await expect(page.locator('[data-task-id="R1"]')).toHaveAttribute(
      "data-task-ref",
      "mdtodo:hwlab-v03-mdtodo:file_plan:R1",
    );
    await expect(page.locator('[data-task-id="R1.1"]')).toBeVisible();
    await expect(page.locator('[data-task-id="R1.1.1"]')).toBeVisible();
    await expect(page.getByTestId("mdtodo-task-detail")).not.toContainText(
      "mdtodo:hwlab-v03-mdtodo:file_plan:R1",
    );
    await expect(page.getByTestId("mdtodo-body-read")).toContainText(
      "Root notes",
    );
    await page.getByTestId("mdtodo-report-link").click();
    await expect(page.getByTestId("mdtodo-report-preview")).toContainText(
      "Report body from fake server",
    );
    await expect(page).toHaveURL(
      /\/projects\/mdtodo\/sources\/hwlab-v03-mdtodo\/files\/file_plan\/tasks\/R1\/reports\/lnk_r11_report/u,
    );
    await page.goBack();
    await expect(page).toHaveURL(/\/files\/file_plan\/tasks\/R1(?:\?|$)/u);
    await page.goForward();
    await expect(page.getByTestId("mdtodo-report-preview")).toContainText(
      "Report body from fake server",
    );
    await page.getByTestId("mdtodo-report-fullscreen").click();
    await expect(
      page.getByTestId("mdtodo-report-fullscreen-dialog"),
    ).toContainText("R1.1 implementation report");
    await page
      .locator('[role="dialog"]')
      .filter({ has: page.getByTestId("mdtodo-report-fullscreen-dialog") })
      .getByLabel("关闭", { exact: true })
      .click();
    await page.getByTestId("mdtodo-info-open").click();
    await expect(page.getByTestId("mdtodo-info-popover")).toContainText(
      "Source",
    );
    await expect(page.getByTestId("mdtodo-info-popover")).toContainText(
      "Task / Window",
    );
    await expect(page.getByTestId("mdtodo-info-popover")).toContainText(
      "Task Ref",
    );
    await expect(page.getByTestId("mdtodo-info-popover")).toContainText(
      "mdtodo:hwlab-v03-mdtodo:file_plan:R1",
    );
    await expect(page.getByTestId("mdtodo-info-copy")).toBeVisible();
    await page
      .getByRole("dialog", { name: "MDTODO 信息" })
      .getByLabel("关闭", { exact: true })
      .click();

    await page.getByTestId("mdtodo-file-select").selectOption("file_feedback");
    await expect(page).toHaveURL(
      /\/projects\/mdtodo\/sources\/hwlab-v03-mdtodo\/files\/file_feedback\/tasks\/R1/u,
    );
    await expect(page.getByTestId("mdtodo-title-read")).toContainText(
      "这是用户的反馈",
    );
    await expect(page.getByTestId("mdtodo-body-read")).toContainText(
      "sys_trip",
    );
    await page.getByTestId("mdtodo-report-link").click();
    await expect(page.getByTestId("mdtodo-report-preview")).toContainText(
      "int_trip",
    );
    await expect(page.getByTestId("mdtodo-report-preview")).toContainText(
      "sys_trip",
    );
    const reportPreBg = await page
      .locator('[data-testid="mdtodo-report-preview"] pre')
      .first()
      .evaluate((node) => getComputedStyle(node).backgroundColor);
    expect(reportPreBg).not.toBe("rgb(15, 23, 42)");
    await page.getByTestId("mdtodo-file-select").selectOption("file_plan");
    await expect(page).toHaveURL(
      /\/projects\/mdtodo\/sources\/hwlab-v03-mdtodo\/files\/file_plan\/tasks\/R1/u,
    );
    await expect(page.getByTestId("mdtodo-title-read")).toContainText(
      "实现项目根导航",
    );

    await page.getByTestId("mdtodo-source-config-open").click();
    await expect(page.getByTestId("mdtodo-source-config-dialog")).toBeVisible();
    await expect(page.getByTestId("mdtodo-source-form-hwpod")).toHaveValue(
      "constart-71freq-c",
    );
    await expect(page.getByTestId("mdtodo-source-form-root")).toHaveValue(
      "docs/MDTODO/",
    );
    await page.getByTestId("mdtodo-source-form-id").fill("draft-source");
    await expect(
      page.getByTestId("mdtodo-source-reindex-dialog"),
    ).toBeDisabled();
    await page.getByTestId("mdtodo-source-form-id").fill("hwlab-v03-mdtodo");
    await expect(
      page.getByTestId("mdtodo-source-reindex-dialog"),
    ).toBeEnabled();
    await page.getByTestId("mdtodo-source-probe").click();
    await expect(page.getByTestId("mdtodo-source-message")).toContainText(
      "Probe ok",
    );
    await page.getByTestId("mdtodo-source-reindex-dialog").click();
    await expect(page.getByTestId("mdtodo-source-message")).toContainText(
      "Reindex 3 files / 5 tasks",
    );
    await page
      .getByRole("dialog", { name: "MDTODO Source" })
      .getByLabel("关闭", { exact: true })
      .click();
    await page.getByTestId("mdtodo-title-read").dblclick();
    await page.getByTestId("mdtodo-edit-title").fill("实现项目根导航 - edited");
    await page.getByTestId("mdtodo-edit-status").selectOption("in_progress");
    await page.getByTestId("mdtodo-edit-save").click();
    await expect(page.getByTestId("mdtodo-task-notice")).toContainText(
      "任务已保存",
    );
    await expect(page.getByTestId("mdtodo-task-detail")).toContainText(
      "实现项目根导航 - edited",
    );
    await expect(page.locator('[data-task-id="R1"]')).toContainText("进行中");
    await page.getByTestId("mdtodo-body-read").dblclick();
    await page
      .getByTestId("mdtodo-body-editor")
      .fill("Body saved from Playwright.");
    await page.getByTestId("mdtodo-body-save").click();
    await expect(page.getByTestId("mdtodo-task-notice")).toContainText(
      "正文已保存",
    );
    await page.getByTestId("mdtodo-new-task-open").click();
    await expect(page.getByTestId("mdtodo-task-create-dialog")).toBeVisible();
    await page.getByTestId("mdtodo-new-title").fill("Playwright child");
    await page.getByTestId("mdtodo-new-body").fill("Child from E2E.");
    await page.getByTestId("mdtodo-add-subtask").click();
    await expect(page.getByTestId("mdtodo-task-notice")).toContainText(
      "任务已创建",
    );
    await expect(page).toHaveURL(/\/tasks\/R1\.2/u);
    await expect(page.locator('[data-task-id="R1.2"]')).toContainText(
      "Playwright child",
    );
    await page.getByTestId("mdtodo-new-task-open").click();
    await page.getByTestId("mdtodo-new-title").fill("Playwright sibling");
    await page.getByTestId("mdtodo-continue-task").click();
    await expect(page.locator('[data-task-id="R1.3"]')).toContainText(
      "Playwright sibling",
    );
    await page.getByTestId("mdtodo-delete-task").click();
    await expect(page.getByTestId("mdtodo-task-notice")).toContainText(
      "再次点击确认删除",
    );
    await page.getByTestId("mdtodo-delete-task").click();
    await expect(page.getByTestId("mdtodo-task-notice")).toContainText(
      "任务已删除",
    );
    await expect(page).toHaveURL(/\/tasks\/R1(?:\?|$)/u);
    await expect(page.locator('[data-task-id="R1.3"]')).toHaveCount(0);
    await page.getByTestId("mdtodo-info-open").click();
    await expect(page.getByTestId("mdtodo-info-popover")).toContainText(
      "ses_project_seed",
    );
    await page
      .getByRole("dialog", { name: "MDTODO 信息" })
      .getByLabel("关闭", { exact: true })
      .click();
    await expect(page.getByTestId("mdtodo-launch-workbench")).toBeEnabled();
    await expect(page.locator("iframe")).toHaveCount(0);
    await saveScreenshot(page, testInfo, "project-management-mdtodo");

    const workbenchPagePromise = page.waitForEvent("popup");
    await page.getByTestId("mdtodo-launch-workbench").click();
    const workbenchPage = await workbenchPagePromise;
    await expect(workbenchPage).toHaveURL(
      /\/workbench\/sessions\/ses_project_launch_/u,
    );
    await expect(workbenchPage.locator("#workspace")).toBeVisible();
    const sessionId =
      workbenchPage.url().match(/\/workbench\/sessions\/([^/?#]+)/u)?.[1] ?? "";
    expect(sessionId).toMatch(/^ses_project_launch_/u);

    const sessionDetail = await page.request.get(
      `/v1/workbench/sessions/${encodeURIComponent(sessionId)}`,
    );
    expect(sessionDetail.ok()).toBeTruthy();
    const sessionPayload = await sessionDetail.json();
    expect(
      sessionPayload.session?.launchContext?.projectId ??
        sessionPayload.session?.metadata?.launchContext?.projectId,
    ).toBe("project_hwlab_v03");
    expect(
      sessionPayload.session?.launchContext?.taskRef ??
        sessionPayload.session?.metadata?.launchContext?.taskRef,
    ).toBe("mdtodo:hwlab-v03-mdtodo:file_plan:R1");
    expect(
      sessionPayload.session?.messageCount ??
        sessionPayload.session?.messages?.length ??
        0,
    ).toBeGreaterThan(0);
    expect(sessionPayload.session?.firstUserMessagePreview ?? "").toContain(
      "MDTODO",
    );

    const links = await page.request.get(
      "/v1/project-management/workbench-links?taskRef=mdtodo:hwlab-v03-mdtodo:file_plan:R1",
    );
    expect(links.ok()).toBeTruthy();
    const linksPayload = await links.json();
    expect(
      linksPayload.links.some(
        (link: { sessionId?: string }) => link.sessionId === sessionId,
      ),
    ).toBeTruthy();
    await saveScreenshot(
      workbenchPage,
      testInfo,
      "project-management-workbench-launch",
    );
  });

  test("restores task 205 deep link and incrementally loads the bounded task tree", async ({
    page,
  }) => {
    const longTasks = Array.from({ length: 205 }, (_, index) => {
      const taskId = `R${index + 1}`;
      return {
        taskRef: `mdtodo:hwlab-v03-mdtodo:file_long:${taskId}`,
        projectId: "project_hwlab_v03",
        sourceId: "hwlab-v03-mdtodo",
        fileRef: "file_long",
        taskId,
        title: `Long tree task ${index + 1}`,
        status: index === 204 ? "in_progress" : "open",
        parentTaskRef: null,
        depth: 0,
        ordinal: index + 1,
        bodyPreview: `Long tree body ${index + 1}`,
        sourceFingerprint: "sha256:e2e-long",
      };
    });
    await page.route(
      "**/v1/project-management/mdtodo/files?**",
      async (route) => {
        const upstream = await route.fetch();
        const body = (await upstream.json()) as {
          files?: Array<Record<string, unknown>>;
        };
        await route.fulfill({
          response: upstream,
          json: {
            ...body,
            files: [
              ...(body.files ?? []),
              {
                sourceId: "hwlab-v03-mdtodo",
                fileRef: "file_long",
                projectId: "project_hwlab_v03",
                relativePath: "docs/MDTODO/long-tree.md",
                title: "205 task fixture",
                taskCount: 205,
                fingerprint: "sha256:e2e-long",
              },
            ],
          },
        });
      },
    );
    await page.route(
      "**/v1/project-management/mdtodo/tasks?**",
      async (route) => {
        const url = new URL(route.request().url());
        if (url.searchParams.get("fileRef") !== "file_long")
          return route.continue();
        const exactRef = url.searchParams.get("taskRef");
        const search = (url.searchParams.get("search") ?? "").toLowerCase();
        const status = url.searchParams.get("status");
        const offset = Number(url.searchParams.get("offset") ?? 0);
        const limit = Number(url.searchParams.get("limit") ?? 100);
        const matched = longTasks.filter(
          (task) =>
            (!exactRef || task.taskRef === exactRef) &&
            (!search ||
              [task.taskId, task.title, task.taskRef].some((value) =>
                value.toLowerCase().includes(search),
              )) &&
            (!status || task.status === status),
        );
        const tasks = matched.slice(offset, offset + limit);
        await route.fulfill({
          status: 200,
          contentType: "application/json",
          json: {
            ok: true,
            contractVersion: "project-management-v1",
            serviceId: "hwlab-project-management",
            tasks,
            page: {
              offset,
              limit,
              maxLimit: 500,
              total: matched.length,
              hasMore: offset + tasks.length < matched.length,
            },
            valuesRedacted: true,
          },
        });
      },
    );
    await page.route(
      "**/v1/project-management/mdtodo/task-detail?**",
      async (route) => {
        const url = new URL(route.request().url());
        const task = longTasks.find(
          (item) => item.taskRef === url.searchParams.get("taskRef"),
        );
        if (!task) return route.continue();
        await route.fulfill({
          status: 200,
          contentType: "application/json",
          json: {
            ok: true,
            contractVersion: "project-management-v1",
            serviceId: "hwlab-project-management",
            task,
            detail: {
              ...task,
              body: task.bodyPreview,
              links: [],
              valuesRedacted: false,
            },
            valuesRedacted: true,
          },
        });
      },
    );

    await page.goto(
      "/projects/mdtodo/sources/hwlab-v03-mdtodo/files/file_long/tasks/R205",
    );
    await expect(page).toHaveURL((url) =>
      url.pathname.endsWith("/files/file_long/tasks/R205"),
    );
    await expect(page.getByTestId("mdtodo-title-read")).toContainText(
      "Long tree task 205",
    );
    await expect(page.locator('[data-task-id="R205"]')).toBeVisible();
    await expect(page.getByTestId("mdtodo-task-page-fact")).toContainText(
      "101 / 205",
    );
    await page.getByTestId("mdtodo-load-more").click();
    await expect(page.getByTestId("mdtodo-task-page-fact")).toContainText(
      "201 / 205",
    );
    await page.getByTestId("mdtodo-load-more").click();
    await expect(page.getByTestId("mdtodo-load-more")).toHaveCount(0);
    await expect(page.locator('[data-task-id="R204"]')).toBeVisible();
    await expect(page).toHaveURL(
      (url) =>
        url.pathname.endsWith("/files/file_long/tasks/R205") &&
        url.searchParams.get("provider") === "codex-api",
    );
  });

  test("keeps an offline HWPOD projection read-only without requesting live reports", async ({
    page,
  }) => {
    let reportPreviewRequests = 0;
    await page.route(
      "**/v1/project-management/mdtodo/task-detail?**",
      async (route) => {
        await route.fulfill({
          status: 200,
          contentType: "application/json",
          json: {
            ok: true,
            contractVersion: "project-management-v1",
            serviceId: "hwlab-project-management",
            status: "partial",
            detail: null,
            detailAuthority: {
              state: "unavailable",
              authority: "project-management-projection",
              sourceKind: "hwpod-workspace",
              code: "hwpod_node_offline",
              message:
                "HWPOD 源当前不可读；页面继续展示最近一次项目投影，正文编辑和报告预览需等待 Node 恢复。",
              retryable: true,
              valuesRedacted: true,
            },
            valuesRedacted: true,
          },
        });
      },
    );
    await page.route(
      "**/v1/project-management/mdtodo/report-preview?**",
      async (route) => {
        reportPreviewRequests += 1;
        await route.fulfill({
          status: 503,
          contentType: "application/json",
          json: {
            ok: false,
            code: "hwpod_node_offline",
            message: "must not be requested",
          },
        });
      },
    );

    await page.goto(
      "/projects/mdtodo/sources/hwlab-v03-mdtodo/files/file_plan/tasks/R1",
    );
    await expect(page.getByTestId("mdtodo-task-detail")).toContainText(
      "offline projection",
    );
    await expect(page.getByTestId("mdtodo-title-read")).toBeDisabled();
    await expect(page.getByTestId("mdtodo-edit-status")).toBeDisabled();
    await expect(page.getByTestId("mdtodo-delete-task")).toBeDisabled();
    await expect(page.getByTestId("mdtodo-new-task-open")).toBeDisabled();
    await expect(page.getByTestId("mdtodo-create-blocker")).toContainText(
      "离线投影",
    );
    await page.getByTestId("mdtodo-body-read").dblclick({ force: true });
    await expect(page.getByTestId("mdtodo-body-editor")).toHaveCount(0);

    await expect(page.getByTestId("mdtodo-report-link")).toBeDisabled();
    await expect(page.getByTestId("mdtodo-task-detail")).toContainText(
      /报告索引|Node 恢复/u,
    );
    expect(reportPreviewRequests).toBe(0);
    await expect(page.getByTestId("mdtodo-launch-workbench")).toBeEnabled();
  });
});
