import { expect, test, type Page } from "@playwright/test";
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";

const DB_WORKDIR = resolve(process.cwd(), "../../packages/db");
const PLAYWRIGHT_RUNTIME_ENV_PATH = resolve(process.cwd(), "e2e/.playwright-runtime.json");
const WEB_ENV_PATHS = [
  resolve(process.cwd(), ".env.local"),
  resolve(process.cwd(), "../../.env.local"),
  resolve(process.cwd(), "../../.env"),
];

function resolveDatabaseUrl(): string {
  const explicitPlaywrightUrl = process.env["PLAYWRIGHT_DATABASE_URL"];
  if (explicitPlaywrightUrl) {
    return explicitPlaywrightUrl;
  }

  if (existsSync(PLAYWRIGHT_RUNTIME_ENV_PATH)) {
    try {
      const runtimeEnv = JSON.parse(readFileSync(PLAYWRIGHT_RUNTIME_ENV_PATH, "utf8")) as {
        PLAYWRIGHT_DATABASE_URL?: string;
        DATABASE_URL?: string;
      };
      if (runtimeEnv.PLAYWRIGHT_DATABASE_URL) {
        return runtimeEnv.PLAYWRIGHT_DATABASE_URL;
      }
      if (runtimeEnv.DATABASE_URL) {
        return runtimeEnv.DATABASE_URL;
      }
    } catch {
      // Fall back to env files if the runtime file is temporarily unavailable.
    }
  }

  for (const envPath of WEB_ENV_PATHS) {
    if (!existsSync(envPath)) {
      continue;
    }

    const envFile = readFileSync(envPath, "utf8");
    for (const rawLine of envFile.split(/\r?\n/u)) {
      const line = rawLine.trim();
      if (!line || line.startsWith("#")) {
        continue;
      }
      if (line.startsWith("PLAYWRIGHT_DATABASE_URL=")) {
        return line.slice("PLAYWRIGHT_DATABASE_URL=".length);
      }
      if (line.startsWith("DATABASE_URL_TEST=")) {
        return line.slice("DATABASE_URL_TEST=".length);
      }
      if (line.startsWith("DATABASE_URL=")) {
        return line.slice("DATABASE_URL=".length);
      }
    }
  }

  const fallbackTestUrl = process.env["DATABASE_URL_TEST"];
  if (fallbackTestUrl) {
    return fallbackTestUrl;
  }

  const fallback = process.env["DATABASE_URL"];
  if (fallback) {
    return fallback;
  }

  throw new Error("DATABASE_URL is not available for the Playwright timeline test.");
}

function runDbJson<T>(body: string): T {
  const script = `
    import { PrismaClient } from "@prisma/client";
    const prisma = new PrismaClient({
      datasources: {
        db: {
          url: process.env.DATABASE_URL,
        },
      },
    });

    try {
      ${body}
    } finally {
      await prisma.$disconnect();
    }
  `;

  const output = execFileSync("node", ["--input-type=module", "-e", script], {
    cwd: DB_WORKDIR,
    env: {
      ...process.env,
      DATABASE_URL: resolveDatabaseUrl(),
    },
    encoding: "utf8",
  }).trim();

  return JSON.parse(output) as T;
}

type TimelineSegmentScenario = {
  assignmentId: string;
  projectId: string;
  projectName: string;
  projectShortCode: string;
  resourceId: string;
  resourceEid: string;
};

type TimelineDemandScenario = {
  demandId: string;
  projectId: string;
  projectName: string;
  projectShortCode: string;
  resourceId: string;
};

function createTimelineSegmentScenario(suffix: string): TimelineSegmentScenario {
  return runDbJson<TimelineSegmentScenario>(`
    const availability = {
      monday: 8,
      tuesday: 8,
      wednesday: 8,
      thursday: 8,
      friday: 8,
      saturday: 0,
      sunday: 0,
    };

    const resource = await prisma.resource.create({
      data: {
        eid: ${JSON.stringify(`e2e.timeline.${suffix}`)},
        displayName: ${JSON.stringify(`E2E Timeline ${suffix}`)},
        email: ${JSON.stringify(`e2e.timeline.${suffix}@capakraken.dev`)},
        chapter: "E2E",
        lcrCents: 5000,
        ucrCents: 9000,
        availability,
        skills: [],
        dynamicFields: {},
        resourceType: "EMPLOYEE",
        chgResponsibility: true,
        rolledOff: false,
        departed: false,
        fte: 1,
      },
      select: { id: true, eid: true },
    });

    const project = await prisma.project.create({
      data: {
        shortCode: ${JSON.stringify(`E2ETL${suffix.slice(-6).toUpperCase()}`)},
        name: ${JSON.stringify(`E2E Timeline Project ${suffix}`)},
        orderType: "CHARGEABLE",
        allocationType: "EXT",
        budgetCents: 2500000,
        startDate: new Date("2026-04-01T00:00:00.000Z"),
        endDate: new Date("2026-04-30T00:00:00.000Z"),
        status: "ACTIVE",
        staffingReqs: [],
        dynamicFields: {},
      },
      select: { id: true, name: true, shortCode: true },
    });

    const assignment = await prisma.assignment.create({
      data: {
        resourceId: resource.id,
        projectId: project.id,
        startDate: new Date("2026-04-06T00:00:00.000Z"),
        endDate: new Date("2026-04-17T00:00:00.000Z"),
        hoursPerDay: 8,
        percentage: 100,
        role: "E2E Role",
        dailyCostCents: 40000,
        status: "ACTIVE",
        metadata: {},
      },
      select: { id: true },
    });

    console.log(JSON.stringify({
      assignmentId: assignment.id,
      projectId: project.id,
      projectName: project.name,
      projectShortCode: project.shortCode,
      resourceId: resource.id,
      resourceEid: resource.eid,
    }));
  `);
}

function createTimelineDemandScenario(suffix: string): TimelineDemandScenario {
  return runDbJson<TimelineDemandScenario>(`
    const availability = {
      monday: 8,
      tuesday: 8,
      wednesday: 8,
      thursday: 8,
      friday: 8,
      saturday: 0,
      sunday: 0,
    };

    const resource = await prisma.resource.create({
      data: {
        eid: ${JSON.stringify(`e2e.timeline.demand.${suffix}`)},
        displayName: ${JSON.stringify(`E2E Timeline Demand Resource ${suffix}`)},
        email: ${JSON.stringify(`e2e.timeline.demand.${suffix}@capakraken.dev`)},
        chapter: "E2E",
        lcrCents: 5000,
        ucrCents: 9000,
        availability,
        skills: [],
        dynamicFields: {},
        resourceType: "EMPLOYEE",
        chgResponsibility: true,
        rolledOff: false,
        departed: false,
        fte: 1,
      },
      select: { id: true },
    });

    const project = await prisma.project.create({
      data: {
        shortCode: ${JSON.stringify(`E2EDM${suffix.slice(-6).toUpperCase()}`)},
        name: ${JSON.stringify(`E2E Timeline Demand ${suffix}`)},
        orderType: "CHARGEABLE",
        allocationType: "EXT",
        budgetCents: 1500000,
        startDate: new Date("2026-04-01T00:00:00.000Z"),
        endDate: new Date("2026-04-30T00:00:00.000Z"),
        status: "ACTIVE",
        staffingReqs: [],
        dynamicFields: {},
      },
      select: { id: true, name: true, shortCode: true },
    });

    const demand = await prisma.demandRequirement.create({
      data: {
        projectId: project.id,
        startDate: new Date("2026-04-07T00:00:00.000Z"),
        endDate: new Date("2026-04-16T00:00:00.000Z"),
        hoursPerDay: 6,
        percentage: 100,
        role: "E2E Open Demand",
        headcount: 2,
        budgetCents: 240000,
        status: "PROPOSED",
        metadata: {},
      },
      select: { id: true },
    });

    await prisma.assignment.create({
      data: {
        resourceId: resource.id,
        projectId: project.id,
        startDate: new Date("2026-04-08T00:00:00.000Z"),
        endDate: new Date("2026-04-10T00:00:00.000Z"),
        hoursPerDay: 4,
        percentage: 50,
        role: "E2E Seeded Assignment",
        dailyCostCents: 20000,
        status: "ACTIVE",
        metadata: {},
      },
      select: { id: true },
    });

    console.log(JSON.stringify({
      demandId: demand.id,
      projectId: project.id,
      projectName: project.name,
      projectShortCode: project.shortCode,
      resourceId: resource.id,
    }));
  `);
}

function cleanupTimelineSegmentScenario(projectId: string, resourceId: string) {
  runDbJson<null>(`
    await prisma.assignment.deleteMany({
      where: { projectId: ${JSON.stringify(projectId)} },
    });

    await prisma.project.deleteMany({
      where: { id: ${JSON.stringify(projectId)} },
    });

    await prisma.resource.deleteMany({
      where: { id: ${JSON.stringify(resourceId)} },
    });

    console.log("null");
  `);
}

function cleanupTimelineDemandScenario(projectId: string, resourceId: string) {
  runDbJson<null>(`
    await prisma.assignment.deleteMany({
      where: { projectId: ${JSON.stringify(projectId)} },
    });

    await prisma.demandRequirement.deleteMany({
      where: { projectId: ${JSON.stringify(projectId)} },
    });

    await prisma.project.deleteMany({
      where: { id: ${JSON.stringify(projectId)} },
    });

    await prisma.resource.deleteMany({
      where: { id: ${JSON.stringify(resourceId)} },
    });

    console.log("null");
  `);
}

function listScenarioAssignments(projectId: string) {
  return runDbJson<Array<{ id: string; startDate: string; endDate: string }>>(`
    const assignments = await prisma.assignment.findMany({
      where: { projectId: ${JSON.stringify(projectId)} },
      orderBy: [{ startDate: "asc" }, { endDate: "asc" }],
      select: { id: true, startDate: true, endDate: true },
    });

    console.log(JSON.stringify(
      assignments.map((entry) => ({
        id: entry.id,
        startDate: entry.startDate.toISOString().slice(0, 10),
        endDate: entry.endDate.toISOString().slice(0, 10),
      })),
    ));
  `);
}

function listScenarioDemands(projectId: string) {
  return runDbJson<Array<{ id: string; startDate: string; endDate: string; headcount: number; status: string }>>(`
    const demands = await prisma.demandRequirement.findMany({
      where: { projectId: ${JSON.stringify(projectId)} },
      orderBy: [{ startDate: "asc" }, { endDate: "asc" }],
      select: { id: true, startDate: true, endDate: true, headcount: true, status: true },
    });

    console.log(JSON.stringify(
      demands.map((entry) => ({
        id: entry.id,
        startDate: entry.startDate.toISOString().slice(0, 10),
        endDate: entry.endDate.toISOString().slice(0, 10),
        headcount: entry.headcount,
        status: entry.status,
      })),
    ));
  `);
}

function readScenarioSnapshot(projectId: string, resourceId: string, resourceEid: string) {
  return runDbJson<{
    resource: { id: string; eid: string; displayName: string } | null;
    project: { id: string; name: string; shortCode: string } | null;
    assignments: Array<{ id: string; startDate: string; endDate: string; status: string }>;
  }>(`
    const [resource, project, assignments] = await Promise.all([
      prisma.resource.findUnique({
        where: { id: ${JSON.stringify(resourceId)} },
        select: { id: true, eid: true, displayName: true },
      }),
      prisma.project.findUnique({
        where: { id: ${JSON.stringify(projectId)} },
        select: { id: true, name: true, shortCode: true },
      }),
      prisma.assignment.findMany({
        where: {
          projectId: ${JSON.stringify(projectId)},
          resourceId: ${JSON.stringify(resourceId)},
        },
        orderBy: [{ startDate: "asc" }, { endDate: "asc" }],
        select: { id: true, startDate: true, endDate: true, status: true },
      }),
    ]);

    console.log(JSON.stringify({
      resource: resource && resource.eid === ${JSON.stringify(resourceEid)}
        ? resource
        : resource,
      project,
      assignments: assignments.map((entry) => ({
        id: entry.id,
        startDate: entry.startDate.toISOString().slice(0, 10),
        endDate: entry.endDate.toISOString().slice(0, 10),
        status: entry.status,
      })),
    }));
  `);
}

async function waitForScenarioAssignments(
  projectId: string,
  expected: Array<{ startDate: string; endDate: string }>,
) {
  await expect
    .poll(
      () =>
        listScenarioAssignments(projectId).map((entry) => ({
          startDate: entry.startDate,
          endDate: entry.endDate,
        })),
      { timeout: 15000 },
    )
    .toEqual(expected);
}

async function dragLocatorBy(page: Page, locator: ReturnType<Page["locator"]>, deltaX: number) {
  const box = await locator.boundingBox();
  expect(box).not.toBeNull();
  if (!box) {
    throw new Error("Expected locator to be visible before dragging");
  }

  const startX = box.x + box.width / 2;
  const startY = box.y + box.height / 2;
  await page.mouse.move(startX, startY);
  await page.mouse.down();
  await page.mouse.move(startX + deltaX, startY, { steps: 8 });
  await page.waitForTimeout(80);

  return box;
}

async function openAllocationContextMenuAtOffset(
  page: Page,
  locator: ReturnType<Page["locator"]>,
  xOffset: number,
) {
  const target = await resolveAllocationContextMenuTarget(locator);
  const box = await readBoundingBox(target);

  await page.mouse.click(
    box.x + Math.min(Math.max(xOffset, 2), Math.max(box.width - 2, 2)),
    box.y + box.height / 2,
    { button: "right" },
  );
}

async function openContextMenuAtCenter(
  page: Page,
  locator: ReturnType<Page["locator"]>,
) {
  const target = await resolveAllocationContextMenuTarget(locator);
  const box = await readBoundingBox(target);
  await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, { button: "right" });
}

async function releaseMouse(page: Page) {
  await page.mouse.up();
  await page.waitForTimeout(120);
}

async function selectProjectFromCombobox(
  page: Page,
  projectShortCode: string,
  projectName: string,
) {
  const projectInput = page.locator('input[placeholder="Search project…"]').last();
  await projectInput.click();
  await projectInput.fill(projectShortCode);

  const optionName = new RegExp(
    `${escapeRegex(projectShortCode)}\\s*.*${escapeRegex(projectName)}`,
    "i",
  );
  await page.getByRole("button", { name: optionName }).last().click();
}

async function dragRowSelection(
  page: Page,
  row: ReturnType<Page["locator"]>,
  startX: number,
  endX: number,
) {
  const rowBox = await readBoundingBox(row);
  const centerY = rowBox.y + Math.min(rowBox.height / 2, rowBox.height - 6);
  await page.mouse.move(startX, centerY);
  await page.mouse.down();
  await page.mouse.move(endX, centerY, { steps: 8 });
  await page.mouse.up();
  await page.waitForTimeout(120);
}

async function readBoundingBox(locator: ReturnType<Page["locator"]>) {
  const box = await locator.boundingBox();
  expect(box).not.toBeNull();
  if (!box) {
    throw new Error("Expected locator to have a bounding box");
  }
  return box;
}

async function resolveAllocationContextMenuTarget(locator: ReturnType<Page["locator"]>) {
  const interactionTarget = locator.locator("[data-allocation-interaction='body']").first();
  return (await interactionTarget.count()) > 0 ? interactionTarget : locator;
}

async function listRenderedAllocationSegments(
  row: ReturnType<Page["locator"]>,
  allocationId?: string,
) {
  const selector = allocationId
    ? `[data-allocation-id="${allocationId}"]`
    : "[data-allocation-id]";
  return row.locator(selector).evaluateAll((elements) =>
    elements.map((element) => {
      const htmlElement = element as HTMLElement;
      const { dataset } = htmlElement;
      return {
        allocationId: dataset.allocationId ?? null,
        segmentIndex: dataset.allocationSegmentIndex ?? null,
        segmentStart: dataset.allocationSegmentStart ?? null,
        segmentEnd: dataset.allocationSegmentEnd ?? null,
        interaction: dataset.allocationInteraction ?? null,
        text: htmlElement.innerText.replace(/\s+/gu, " ").trim(),
      };
    }),
  );
}

function escapeRegex(value: string) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

async function signInAsAdmin(page: Page) {
  await page.goto("/auth/signin");
  await page.fill('input[type="email"]', "admin@capakraken.dev");
  await page.fill('input[type="password"]', "admin123");
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL(/\/(dashboard|resources)/);
}

async function findVisibleTimelineEntryId(
  page: Page,
  selector: string,
  minimumWidth = 24,
) {
  return page.locator(selector).evaluateAll((elements, minimum) => {
    for (const element of elements) {
      if (!(element instanceof HTMLElement)) continue;
      const id = element.dataset.allocationId ?? element.dataset.timelineProjectId;
      if (!id) continue;

      const rect = element.getBoundingClientRect();
      const mostlyVisible =
        rect.width >= minimum &&
        rect.height >= 12 &&
        rect.right > 96 &&
        rect.left < window.innerWidth - 96 &&
        rect.bottom > 0 &&
        rect.top < window.innerHeight;

      if (mostlyVisible) {
        return id;
      }
    }

    return null;
  }, minimumWidth);
}

type VisibleAllocationSegment = {
  allocationId: string;
  segmentStart: string | null;
  segmentEnd: string | null;
};

function allocationSegmentSelector(segment: VisibleAllocationSegment) {
  const parts = [
    `[data-timeline-entry-type='allocation'][data-allocation-id='${segment.allocationId}']`,
  ];
  if (segment.segmentStart) {
    parts.push(`[data-allocation-segment-start='${segment.segmentStart}']`);
  }
  if (segment.segmentEnd) {
    parts.push(`[data-allocation-segment-end='${segment.segmentEnd}']`);
  }
  return parts.join("");
}

async function findVisibleAllocationSegmentForResize(
  page: Page,
  selector: string,
): Promise<VisibleAllocationSegment | null> {
  return page.locator(selector).evaluateAll((elements) => {
    const scrollContainer = document.querySelector<HTMLElement>(
      "div.app-surface.relative.z-0.flex-1.overflow-auto",
    );
    const stickyHeaderBottom = scrollContainer
      ? Array.from(scrollContainer.querySelectorAll<HTMLElement>(".sticky.top-0")).reduce(
        (maxBottom, element) => Math.max(maxBottom, element.getBoundingClientRect().bottom),
        0,
      )
      : 0;
    const safeTop = stickyHeaderBottom > 0 ? stickyHeaderBottom + 8 : 48;
    const candidates: Array<{
      allocationId: string;
      segmentStart: string | null;
      segmentEnd: string | null;
      score: number;
    }> = [];
    let fallback: { allocationId: string; segmentStart: string | null; segmentEnd: string | null } | null =
      null;

    for (const element of elements) {
      if (!(element instanceof HTMLElement)) continue;
      const allocationId = element.dataset.allocationId;
      if (!allocationId) continue;

      const rect = element.getBoundingClientRect();
      const isVisible =
        rect.width >= 24 &&
        rect.height >= 10 &&
        rect.right > 48 &&
        rect.left < window.innerWidth - 48 &&
        rect.bottom > safeTop &&
        rect.top < window.innerHeight;

      if (!isVisible) {
        continue;
      }

      fallback ??= {
        allocationId,
        segmentStart: element.dataset.allocationSegmentStart ?? null,
        segmentEnd: element.dataset.allocationSegmentEnd ?? null,
      };

      const isPreferred =
        rect.width >= 36 &&
        rect.width <= 220 &&
        rect.left >= 48 &&
        rect.right <= window.innerWidth - 48 &&
        rect.top >= safeTop;

      if (!isPreferred) {
        continue;
      }

      const centerX = rect.left + rect.width / 2;
      const widthPenalty = Math.abs(rect.width - 96);
      const centerPenalty = Math.abs(centerX - window.innerWidth / 2) / 4;
      candidates.push({
        allocationId,
        segmentStart: element.dataset.allocationSegmentStart ?? null,
        segmentEnd: element.dataset.allocationSegmentEnd ?? null,
        score: widthPenalty + centerPenalty,
      });
    }

    candidates.sort((a, b) => a.score - b.score);
    return candidates[0] ?? fallback;
  });
}

async function measureProjectBarDragGap(page: Page, locatorString: string) {
  const bar = page.locator(locatorString);
  await bar.scrollIntoViewIfNeeded();

  const box = await bar.boundingBox();
  expect(box).not.toBeNull();
  if (!box) {
    throw new Error("Expected a visible project bar for drag measurement");
  }

  const anchorOffsetX = Math.min(Math.max(box.width * 0.35, 16), box.width - 16);
  const pointerY = box.y + box.height / 2;
  const startX = box.x + anchorOffsetX;
  const dragDistance = 132;
  const gaps: number[] = [];
  const lefts: number[] = [];

  await page.mouse.move(startX, pointerY);
  await page.mouse.down();

  for (let step = 1; step <= 8; step += 1) {
    const targetX = startX + (dragDistance * step) / 8;
    await page.mouse.move(targetX, pointerY, { steps: 1 });
    await page.waitForTimeout(16);

    const currentBox = await bar.boundingBox();
    if (!currentBox) continue;

    lefts.push(currentBox.x);
    gaps.push(Math.abs(currentBox.x - (targetX - anchorOffsetX)));
  }

  await page.mouse.up();

  return {
    maxGap: Math.max(...gaps),
    movedDistance: Math.max(...lefts) - box.x,
  };
}

async function measureAllocationDragGap(page: Page, locatorString: string) {
  const bar = page.locator(locatorString);
  await bar.scrollIntoViewIfNeeded();

  const box = await bar.boundingBox();
  expect(box).not.toBeNull();
  if (!box) {
    throw new Error("Expected a visible allocation bar for drag measurement");
  }

  const anchorOffsetX = Math.min(Math.max(box.width / 2, 18), box.width - 18);
  const pointerY = box.y + box.height / 2;
  const startX = box.x + anchorOffsetX;
  const dragDistance = 108;
  const gaps: number[] = [];
  const lefts: number[] = [];

  await page.mouse.move(startX, pointerY);
  await page.mouse.down();

  for (let step = 1; step <= 8; step += 1) {
    const targetX = startX + (dragDistance * step) / 8;
    await page.mouse.move(targetX, pointerY, { steps: 1 });
    await page.waitForTimeout(16);

    const currentBox = await bar.boundingBox();
    if (!currentBox) continue;

    lefts.push(currentBox.x);
    gaps.push(Math.abs(currentBox.x - (targetX - anchorOffsetX)));
  }

  await page.mouse.up();

  return {
    maxGap: Math.max(...gaps),
    movedDistance: Math.max(...lefts) - box.x,
  };
}

async function measureAllocationResizeGap(page: Page, locatorString: string) {
  const bar = page.locator(locatorString);
  await bar.scrollIntoViewIfNeeded();

  const box = await bar.boundingBox();
  expect(box).not.toBeNull();
  if (!box) {
    throw new Error("Expected a visible allocation bar for resize measurement");
  }

  const pointerY = box.y + box.height / 2;
  const startX = box.x + box.width - 3;
  const resizeDistance = 96;
  const widths: number[] = [];
  const rightEdges: number[] = [];

  await page.mouse.move(startX, pointerY);
  await page.mouse.down();

  for (let step = 1; step <= 8; step += 1) {
    const targetX = startX + (resizeDistance * step) / 8;
    await page.mouse.move(targetX, pointerY, { steps: 1 });
    await page.waitForTimeout(16);

    const currentBox = await bar.boundingBox();
    if (!currentBox) continue;

    widths.push(currentBox.width);
    rightEdges.push(currentBox.x + currentBox.width);
  }

  await page.mouse.up();

  return {
    widthGain: Math.max(...widths) - box.width,
    rightEdgeGain: Math.max(...rightEdges) - (box.x + box.width),
  };
}

async function measureAllocationResizeStartGap(page: Page, locatorString: string) {
  const bar = page.locator(locatorString);
  await bar.scrollIntoViewIfNeeded();

  const box = await bar.boundingBox();
  expect(box).not.toBeNull();
  if (!box) {
    throw new Error("Expected a visible allocation bar for resize-start measurement");
  }

  const pointerY = box.y + box.height / 2;
  const startX = box.x + 3;
  const resizeDistance = 72;
  const widths: number[] = [];
  const leftEdges: number[] = [];

  await page.mouse.move(startX, pointerY);
  await page.mouse.down();

  for (let step = 1; step <= 8; step += 1) {
    const targetX = startX - (resizeDistance * step) / 8;
    await page.mouse.move(targetX, pointerY, { steps: 1 });
    await page.waitForTimeout(16);

    const currentBox = await bar.boundingBox();
    if (!currentBox) continue;

    widths.push(currentBox.width);
    leftEdges.push(currentBox.x);
  }

  await page.mouse.up();

  return {
    widthGain: Math.max(...widths) - box.width,
    leftEdgeGain: box.x - Math.min(...leftEdges),
  };
}

async function switchToProjectView(page: Page, readySelector?: string) {
  await page.getByRole("button", { name: "Project view" }).click();
  if (readySelector) {
    await expect(page.locator(readySelector).first()).toBeVisible();
  } else {
    await expect
      .poll(async () => {
        const projectRows = await page.getByTestId("timeline-project-resource-row-canvas").count();
        const projectBars = await page.locator("[data-timeline-entry-type='project-bar']").count();
        const demandBars = await page.locator("[data-timeline-entry-type='demand']").count();
        const emptyStates = await page.getByText(/No projects in this time range/).count();
        return projectRows + projectBars + demandBars + emptyStates;
      }, { timeout: 10_000 })
      .not.toBe(0);
  }
  await expect(page.getByTestId("timeline-resource-row-canvas")).toHaveCount(0);
}

async function switchToResourceView(page: Page, readySelector?: string) {
  await page.getByRole("button", { name: "Resource view" }).click();
  if (readySelector) {
    await expect(page.locator(readySelector).first()).toBeVisible();
  } else {
    await expect(page.getByTestId("timeline-resource-row-canvas").first()).toBeVisible();
  }
  await expect(page.getByTestId("timeline-project-resource-row-canvas")).toHaveCount(0);
}

async function ensureOpenDemandVisibilityEnabled(page: Page) {
  await page.evaluate(() => {
    const raw = window.localStorage.getItem("capakraken_prefs");
    const parsed = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
    window.localStorage.setItem(
      "capakraken_prefs",
      JSON.stringify({
        ...parsed,
        showDemandProjects: true,
      }),
    );
  });
  await page.reload({ waitUntil: "domcontentloaded" });
}

test.describe("Timeline", () => {
  test.describe.configure({ mode: "serial" });

  test.beforeEach(async ({ page }) => {
    await page.addInitScript(() => {
      localStorage.setItem("capakraken_theme", JSON.stringify({ mode: "dark" }));
      localStorage.setItem(
        "capakraken_prefs",
        JSON.stringify({
          hideCompletedProjects: true,
          timelineDisplayMode: "strip",
          heatmapColorScheme: "green-red",
          showDemandProjects: true,
          blinkOverbookedDays: false,
        }),
      );
    });
    await signInAsAdmin(page);
    await page.goto("/timeline");
  });

  test("loads and displays the timeline", async ({ page }) => {
    await expect(page.locator("text=Resource view")).toBeVisible();
    await expect(page.locator("text=Project view")).toBeVisible();
    await expect(page.getByRole("button", { name: /All Clients|Clients:/ })).toBeVisible();
    await expect(page.getByRole("button", { name: /All Chapters|Chapters:/ })).toBeVisible();
    await expect(page.getByRole("button", { name: /All people|People:/ })).toBeVisible();
    // Timeline canvas should be visible
    await expect(page.locator("div.app-surface.relative.flex-1.overflow-auto")).toBeVisible();
  });

  test("can switch between resource and project view", async ({ page }) => {
    await switchToProjectView(page);
    await expect(
      page.locator("text=0 projects").or(page.locator("text=/\\d+ projects/")),
    ).toBeVisible();
    await switchToResourceView(page);
    await expect(page.locator("text=/\\d+ resources/")).toBeVisible();
  });

  test("view toggle stays disabled until the initial timeline load becomes interactive", async ({ page }) => {
    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineSegmentScenario(suffix);

    try {
      await page.goto(
        `/timeline?startDate=2026-04-01&days=31&eids=${scenario.resourceEid}`,
        { waitUntil: "domcontentloaded" },
      );

      const projectButton = page.getByRole("button", { name: "Project view" });
      const resourceButton = page.getByRole("button", { name: "Resource view" });
      const resourceRowSelector =
        `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`;
      const projectRowSelector =
        `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;

      await expect(projectButton).toBeDisabled();
      await expect(resourceButton).toBeDisabled();

      await expect(page.locator(resourceRowSelector)).toBeVisible();
      await expect(projectButton).toBeEnabled();
      await expect(resourceButton).toBeEnabled();
      await expect(resourceButton).toHaveAttribute("aria-pressed", "true");

      await projectButton.click();

      await expect(page.locator(projectRowSelector)).toBeVisible();
      await expect(projectButton).toHaveAttribute("aria-pressed", "true");
      await expect(resourceButton).toHaveAttribute("aria-pressed", "false");
    } finally {
      cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
    }
  });

  test("can navigate forward and back", async ({ page }) => {
    const todayBtn = page.locator("button", { hasText: "Today" });
    await expect(todayBtn).toBeVisible();
    await page.locator("button", { hasText: "›" }).click();
    await page.locator("button", { hasText: "‹" }).click();
    await todayBtn.click();
  });

  test("keeps timeline data populated after navigating from allocations", async ({ page }) => {
    await page.goto("/allocations");
    await expect(
      page.locator("h1").filter({ hasText: /Allocations|Planning/i }),
    ).toBeVisible({ timeout: 10000 });

    await page.locator('nav a >> text="Timeline"').first().click();
    await expect(page).toHaveURL(/\/timeline/);
    await expect(
      page.locator(".app-toolbar").getByText(/[1-9]\d* resources · [1-9]\d* allocations/),
    ).toBeVisible({ timeout: 10000 });
    await expect(page.getByTestId("timeline-resource-row-canvas").first()).toBeVisible({
      timeout: 10000,
    });
    await expect(page.locator("text=No allocations in this time range")).not.toBeVisible();
  });

  test("filter panel opens and closes", async ({ page }) => {
    await page.locator("button", { hasText: "Filter" }).click();
    await expect(page.getByRole("heading", { name: "Filters" })).toBeVisible();
    await expect(page.getByPlaceholder("Search projects…")).toBeVisible();
    await page.keyboard.press("Escape");
    await expect(page.getByRole("heading", { name: "Filters" })).not.toBeVisible();
  });

  test("shows placeholder bars for unassigned allocations", async ({ page }) => {
    // Filter to show placeholders (enabled by default)
    // The timeline should have at least one dashed placeholder bar from seed data
    await page.waitForSelector(".overflow-auto", { state: "visible" });
    // Check that the timeline loaded (resource rows or empty state visible)
    await expect(
      page.locator(".app-toolbar").getByText(/\d+ resources · \d+ allocations/),
    ).toBeVisible();
  });

  test("clicking a placeholder opens the fill placeholder modal", async ({ page }) => {
    // Wait for timeline to load
    await page.waitForSelector(".overflow-auto");
    await page.waitForTimeout(1000); // let tRPC queries settle

    // Try to find and click a placeholder bar (dashed border style)
    const placeholderBar = page.locator("[style*='dashed']").first();
    if ((await placeholderBar.count()) > 0) {
      await placeholderBar.click();
      await expect(
        page.locator("text=Fill Placeholder").or(page.locator("text=Assign Resource")),
      ).toBeVisible();
      await page.keyboard.press("Escape");
    }
  });

  test("resource and project views keep tooltips opaque in dark mode and support right click", async ({
    page,
  }) => {
    await page.waitForSelector(".overflow-auto", { state: "visible" });
    await page.waitForTimeout(1000);

    const heatmapTooltip = page
      .locator("div.fixed.pointer-events-none.rounded-xl.border.border-gray-800")
      .first();
    const allocationPopoverField = page.getByText("Hours / day");

    const resourceHoverTarget = page.getByTestId("timeline-resource-row-canvas").first();
    const resourceHoverBox = await resourceHoverTarget.boundingBox();
    expect(resourceHoverBox).not.toBeNull();
    if (!resourceHoverBox) {
      throw new Error("Expected a resource timeline row canvas to be available");
    }
    await page.mouse.move(resourceHoverBox.x + 120, resourceHoverBox.y + 20);
    await expect(heatmapTooltip).toBeVisible();
    await expect
      .poll(async () => {
        return heatmapTooltip.evaluate((element) => getComputedStyle(element).backgroundColor);
      })
      .toBe("rgba(3, 7, 18, 0.96)");

    const resourceAllocation = page
      .locator("[data-timeline-entry-type='allocation'][data-allocation-id]")
      .first();
    await resourceAllocation.click({ button: "right" });
    await expect(allocationPopoverField).toBeVisible();
    await page.mouse.click(40, 40);

    await switchToProjectView(page);

    const projectHoverTarget = page.getByTestId("timeline-project-resource-row-canvas").first();
    const projectHoverBox = await projectHoverTarget.boundingBox();
    const projectAllocation = page.locator("div[style*='top: 2px'][style*='bottom: 2px']").nth(1);
    const projectAllocationBox = await projectAllocation.boundingBox();
    expect(projectHoverBox).not.toBeNull();
    expect(projectAllocationBox).not.toBeNull();
    if (!projectHoverBox) {
      throw new Error("Expected a project timeline row canvas to be available");
    }
    if (!projectAllocationBox) {
      throw new Error("Expected a project allocation block to be available");
    }
    await page.mouse.move(projectAllocationBox.x + (projectAllocationBox.width / 2), projectHoverBox.y + 20);
    await expect(heatmapTooltip).toBeVisible();
    await expect
      .poll(async () => {
        return heatmapTooltip.evaluate((element) => getComputedStyle(element).backgroundColor);
      })
      .toBe("rgba(3, 7, 18, 0.96)");

    await projectAllocation.click({ button: "right" });
    await expect(allocationPopoverField).toBeVisible();
  });

  test("resource view right click resolves allocation popovers instead of getting stuck on loading", async ({
    page,
  }) => {
    await page.waitForSelector(".overflow-auto", { state: "visible" });
    await expect(
      page.locator("[data-timeline-entry-type='allocation'][data-allocation-id]").first(),
    ).toBeVisible();

    const allocation = page
      .locator("[data-timeline-entry-type='allocation'][data-allocation-id]")
      .first();
    await allocation.click({ button: "right" });

    await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0, { timeout: 2_000 });
    const popover = page.getByTestId("timeline-allocation-popover");
    await expect(popover).toBeVisible();
    await expect(page.getByTestId("timeline-allocation-popover-error")).toHaveCount(0);
    await expect(page.getByTestId("timeline-allocation-popover-unavailable")).toHaveCount(0);
    await expect(popover.getByText("Hours / day")).toBeVisible();
    await expect(popover.getByRole("button", { name: "Open Project Panel →" })).toBeVisible();
  });

  test("right clicking a project header strip opens the project panel", async ({ page }) => {
    await switchToProjectView(page);

    const projectBar = page
      .locator("[data-timeline-entry-type='project-bar'][data-timeline-project-id]")
      .first();
    await expect(projectBar).toBeVisible();

    await projectBar.click({ button: "right" });
    await expect(page.getByText("Project Details")).toBeVisible();
    await expect(page.getByRole("heading", { level: 2 })).toBeVisible();

    await page.keyboard.press("Escape");
    await expect(page.getByText("Project Details")).not.toBeVisible();
  });

  test("shows resolved holiday overlays in the resource timeline and exposes the holiday name in the tooltip", async ({
    page,
  }) => {
    await page.goto("/timeline?startDate=2026-04-01&days=14&eids=bruce.banner", {
      waitUntil: "domcontentloaded",
    });

    const row = page.locator('[data-testid="timeline-resource-row-canvas"][data-resource-eid="bruce.banner"]').first();
    await expect(row).toBeVisible();

    const holidayBlock = row.locator(
      '[data-testid="timeline-vacation-block"][data-vacation-type="PUBLIC_HOLIDAY"][data-vacation-note="Karfreitag"]',
    ).first();
    await expect(holidayBlock).toBeVisible();

    const rowBox = await row.boundingBox();
    const holidayBox = await holidayBlock.boundingBox();
    expect(rowBox).not.toBeNull();
    expect(holidayBox).not.toBeNull();

    if (!rowBox || !holidayBox) {
      throw new Error("Expected timeline row and holiday block bounding boxes to be available");
    }

    await row.hover({
      position: {
        x: holidayBox.x - rowBox.x + holidayBox.width / 2,
        y: holidayBox.y - rowBox.y + Math.min(holidayBox.height / 2, rowBox.height - 4),
      },
    });

    const holidayTooltip = page
      .locator("div.fixed.pointer-events-none.rounded-xl.border.border-amber-700\\/50")
      .or(page.locator("div.fixed.pointer-events-none.rounded-xl").filter({ hasText: "Karfreitag" }))
      .first();

    await expect(holidayTooltip).toBeVisible();
    await expect(holidayTooltip).toContainText("Karfreitag");
    await expect(holidayTooltip).toContainText("3 April 2026");
  });

  test("allocation context popover stays interactive after pointer moves across the timeline canvas", async ({
    page,
  }) => {
    await expect(
      page.locator("[data-timeline-entry-type='allocation'][data-allocation-id]").first(),
    ).toBeVisible();

    const allocationSegment = await findVisibleAllocationSegmentForResize(
      page,
      "[data-timeline-entry-type='allocation'][data-allocation-id]",
    );
    expect(allocationSegment).toBeTruthy();

    const allocation = page.locator(allocationSegmentSelector(allocationSegment!));
    await openContextMenuAtCenter(page, allocation);

    const cancelButton = page.getByRole("button", { name: "Cancel" }).last();
    await expect(cancelButton).toBeVisible();

    const resourceRow = page.getByTestId("timeline-resource-row-canvas").first();
    const resourceRowBox = await resourceRow.boundingBox();
    expect(resourceRowBox).not.toBeNull();
    if (!resourceRowBox) {
      throw new Error("Expected a resource timeline row canvas to be available");
    }

    await page.mouse.move(resourceRowBox.x + 160, resourceRowBox.y + 20);
    await expect(cancelButton).toBeVisible();

    await cancelButton.click();
    await expect(cancelButton).not.toBeVisible();
  });

  test("timeline overlays react predictably to viewport changes", async ({ page }) => {
    const scrollContainer = page.locator("div.app-surface.relative.z-0.flex-1.overflow-auto");
    await expect(scrollContainer).toBeVisible();

    const resourceLabel = page.locator("[data-resource-hover-id]").first();
    await resourceLabel.hover();

    const hoverCard = page.locator("[data-resource-hover-card='true']");
    await expect(hoverCard).toBeVisible();

    const initialTop = await hoverCard.evaluate((element) => element.getBoundingClientRect().top);

    await scrollContainer.evaluate((element) => {
      element.scrollTop = Math.min(220, element.scrollHeight);
      element.dispatchEvent(new Event("scroll", { bubbles: true }));
    });

    await expect(hoverCard).toBeVisible();
    await expect
      .poll(async () => hoverCard.evaluate((element) => element.getBoundingClientRect().top))
      .not.toBe(initialTop);

    await page.mouse.move(8, 8);
    await expect(hoverCard).not.toBeVisible();

    const allocationSegment = await findVisibleAllocationSegmentForResize(
      page,
      "[data-timeline-entry-type='allocation'][data-allocation-id]",
    );
    expect(allocationSegment).toBeTruthy();

    const allocation = page.locator(allocationSegmentSelector(allocationSegment!));
    await openContextMenuAtCenter(page, allocation);

    const cancelButton = page.getByRole("button", { name: "Cancel" }).last();
    await expect(cancelButton).toBeVisible();

    await scrollContainer.evaluate((element) => {
      element.scrollTop = Math.min(element.scrollTop + 160, element.scrollHeight);
      element.dispatchEvent(new Event("scroll", { bubbles: true }));
    });

    await expect(cancelButton).not.toBeVisible();
  });

  test("timeline allocation popovers close cleanly across view switches", async ({ page }) => {
    const allocation = page
      .locator("[data-timeline-entry-type='allocation'][data-allocation-id]")
      .first();
    await expect(allocation).toBeVisible();
    await openContextMenuAtCenter(page, allocation);

    const allocationPopover = page.getByTestId("timeline-allocation-popover");
    await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0, {
      timeout: 2_000,
    });
    await expect(allocationPopover).toBeVisible();

    await switchToProjectView(page);
    await expect(allocationPopover).toHaveCount(0);
    await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0);
    await expect(page.getByTestId("timeline-allocation-popover-error")).toHaveCount(0);
    await expect(page.getByTestId("timeline-allocation-popover-unavailable")).toHaveCount(0);
  });

  test("project bars stay attached to the pointer during fast drag", async ({ page }) => {
    await switchToProjectView(page);
    await expect(page.locator("[data-timeline-entry-type='project-bar']").first()).toBeVisible();

    const projectId = await findVisibleTimelineEntryId(
      page,
      "[data-timeline-entry-type='project-bar'][data-timeline-project-id]",
      24,
    );
    expect(projectId).toBeTruthy();

    const result = await measureProjectBarDragGap(
      page,
      `[data-timeline-entry-type='project-bar'][data-timeline-project-id='${projectId}']`,
    );

    expect(result.movedDistance).toBeGreaterThan(72);
    expect(result.maxGap).toBeLessThan(24);
  });

  test("resource allocation bars stay attached to the pointer during drag", async ({ page }) => {
    await page.goto("/timeline?startDate=2026-04-01&days=31", {
      waitUntil: "domcontentloaded",
    });
    await expect(
      page.locator("[data-timeline-entry-type='allocation'][data-allocation-id]").first(),
    ).toBeVisible();

    const allocationSegment = await findVisibleAllocationSegmentForResize(
      page,
      "[data-timeline-entry-type='allocation'][data-allocation-id]",
    );
    expect(allocationSegment).toBeTruthy();

    const result = await measureAllocationDragGap(
      page,
      allocationSegmentSelector(allocationSegment!),
    );

    expect(result.movedDistance).toBeGreaterThan(56);
    expect(result.maxGap).toBeLessThan(24);
  });

  test("allocation resize shows a live preview before mouseup", async ({
    page,
  }) => {
    await page.goto("/timeline?startDate=2026-04-01&days=31", {
      waitUntil: "domcontentloaded",
    });
    await expect(
      page.locator("[data-timeline-entry-type='allocation'][data-allocation-id]").first(),
    ).toBeVisible();

    const allocationSegment = await findVisibleAllocationSegmentForResize(
      page,
      "[data-timeline-entry-type='allocation'][data-allocation-id]",
    );
    expect(allocationSegment).toBeTruthy();

    const result = await measureAllocationResizeGap(
      page,
      allocationSegmentSelector(allocationSegment!),
    );

    expect(result.widthGain).toBeGreaterThan(64);
    expect(result.rightEdgeGain).toBeGreaterThan(48);
  });

  test("allocation resize cancels cleanly when the window loses focus mid-drag", async ({
    page,
  }) => {
    await page.goto("/timeline?startDate=2026-04-01&days=31", {
      waitUntil: "domcontentloaded",
    });
    await expect(
      page.locator("[data-timeline-entry-type='allocation'][data-allocation-id]").first(),
    ).toBeVisible();

    const allocationSegment = await findVisibleAllocationSegmentForResize(
      page,
      "[data-timeline-entry-type='allocation'][data-allocation-id]",
    );
    expect(allocationSegment).toBeTruthy();

    const allocation = page.locator(allocationSegmentSelector(allocationSegment!));
    await allocation.scrollIntoViewIfNeeded();

    const initialBox = await readBoundingBox(allocation);
    const pointerY = initialBox.y + initialBox.height / 2;
    const startX = initialBox.x + initialBox.width - 3;

    await page.mouse.move(startX, pointerY);
    await page.mouse.down();
    await page.mouse.move(startX + 72, pointerY, { steps: 6 });

    await expect
      .poll(async () => {
        const box = await allocation.boundingBox();
        return box ? Math.round(box.width) : null;
      })
      .toBeGreaterThan(Math.round(initialBox.width + 36));

    await page.evaluate(() => {
      window.dispatchEvent(new Event("blur"));
    });

    await expect
      .poll(async () => {
        const box = await allocation.boundingBox();
        return box ? Math.round(box.width) : null;
      })
      .toBe(Math.round(initialBox.width));

    await page.mouse.up();

    const secondResize = await measureAllocationResizeGap(
      page,
      allocationSegmentSelector(allocationSegment!),
    );
    expect(secondResize.widthGain).toBeGreaterThan(64);
    expect(secondResize.rightEdgeGain).toBeGreaterThan(48);
  });

  test("allocation start resize shows a live preview before mouseup", async ({
    page,
  }) => {
    await page.goto("/timeline?startDate=2026-04-01&days=31", {
      waitUntil: "domcontentloaded",
    });
    await expect(
      page.locator("[data-timeline-entry-type='allocation'][data-allocation-id]").first(),
    ).toBeVisible();

    const allocationSegment = await findVisibleAllocationSegmentForResize(
      page,
      "[data-timeline-entry-type='allocation'][data-allocation-id]",
    );
    expect(allocationSegment).toBeTruthy();

    const result = await measureAllocationResizeStartGap(
      page,
      allocationSegmentSelector(allocationSegment!),
    );

    expect(result.widthGain).toBeGreaterThan(48);
    expect(result.leftEdgeGain).toBeGreaterThan(36);
  });

  test("project view resource allocations resize with a live preview before mouseup", async ({
    page,
  }) => {
    test.setTimeout(60_000);
    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineSegmentScenario(suffix);

    try {
      await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, {
        waitUntil: "domcontentloaded",
      });
      const resourceRowSelector =
        `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`;
      const projectRowSelector =
        `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
      const projectAllocationSelector =
        `${projectRowSelector} [data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`;

      await expect(page.locator(resourceRowSelector)).toBeVisible();
      await expect(
        page.locator(
          `[data-timeline-entry-type="allocation"][data-allocation-id="${scenario.assignmentId}"]`,
        ).first(),
      ).toBeVisible();

      await switchToProjectView(page, projectRowSelector);
      await expect(page.getByText(scenario.projectName).first()).toBeVisible();

      const projectAllocation = page.locator(projectAllocationSelector).first();
      await expect(projectAllocation).toBeVisible();
      await expect
        .poll(async () =>
          projectAllocation.evaluate((element) => ({
            segmentStart: element.getAttribute("data-allocation-segment-start"),
            segmentEnd: element.getAttribute("data-allocation-segment-end"),
          })),
        )
        .toEqual({ segmentStart: null, segmentEnd: null });

      const resizeEnd = await measureAllocationResizeGap(page, projectAllocationSelector);
      expect(resizeEnd.widthGain).toBeGreaterThan(64);
      expect(resizeEnd.rightEdgeGain).toBeGreaterThan(48);
      let rightResizeAssignments: Array<{ id: string; startDate: string; endDate: string }> = [];
      await expect
        .poll(() => {
          rightResizeAssignments = listScenarioAssignments(scenario.projectId);
          if (rightResizeAssignments.length !== 1) {
            return null;
          }

          const [assignment] = rightResizeAssignments;
          if (!assignment || assignment.id !== scenario.assignmentId) {
            return null;
          }

          return assignment.endDate;
        }, { timeout: 15_000 })
        .not.toBe("2026-04-17");
      expect(rightResizeAssignments).toHaveLength(1);
      expect(rightResizeAssignments[0]?.id).toBe(scenario.assignmentId);
      expect(rightResizeAssignments[0]?.startDate).toBe("2026-04-06");
      expect(rightResizeAssignments[0]!.endDate > "2026-04-17").toBe(true);

      const resizeStart = await measureAllocationResizeStartGap(page, projectAllocationSelector);
      expect(resizeStart.widthGain).toBeGreaterThan(48);
      expect(resizeStart.leftEdgeGain).toBeGreaterThan(36);
      let leftResizeAssignments: Array<{ id: string; startDate: string; endDate: string }> = [];
      await expect
        .poll(() => {
          leftResizeAssignments = listScenarioAssignments(scenario.projectId);
          if (leftResizeAssignments.length !== 1) {
            return null;
          }

          const [assignment] = leftResizeAssignments;
          if (!assignment || assignment.id !== scenario.assignmentId) {
            return null;
          }

          return assignment.startDate;
        }, { timeout: 15_000 })
        .not.toBe("2026-04-06");
      expect(leftResizeAssignments).toHaveLength(1);
      expect(leftResizeAssignments[0]?.id).toBe(scenario.assignmentId);
      expect(leftResizeAssignments[0]!.startDate < "2026-04-06").toBe(true);
      expect(leftResizeAssignments[0]?.endDate).toBe(rightResizeAssignments[0]?.endDate);
    } finally {
      cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
    }
  });

  test("project view demand bars resize with a live preview before mouseup", async ({ page }) => {
    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineDemandScenario(suffix);

    try {
      await page.goto(
        `/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`,
        { waitUntil: "domcontentloaded" },
      );
      await ensureOpenDemandVisibilityEnabled(page);
      const demandRowSelector =
        `[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`;
      const demandSelector =
        `${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`;

      await switchToProjectView(page, demandRowSelector);
      await expect(page.getByText(scenario.projectName).first()).toBeVisible();
      await expect(page.locator(demandSelector)).toBeVisible();

      const resizeEnd = await measureAllocationResizeGap(page, demandSelector);
      expect(resizeEnd.widthGain).toBeGreaterThan(48);
      expect(resizeEnd.rightEdgeGain).toBeGreaterThan(36);

      let rightResizeDemands: Array<{
        id: string;
        startDate: string;
        endDate: string;
        headcount: number;
        status: string;
      }> = [];
      await expect
        .poll(() => {
          rightResizeDemands = listScenarioDemands(scenario.projectId);
          if (rightResizeDemands.length !== 1) {
            return null;
          }

          const [demand] = rightResizeDemands;
          if (!demand || demand.id !== scenario.demandId) {
            return null;
          }

          return demand.endDate;
        }, { timeout: 15_000 })
        .not.toBe("2026-04-16");
      expect(rightResizeDemands).toHaveLength(1);
      expect(rightResizeDemands[0]?.id).toBe(scenario.demandId);
      expect(rightResizeDemands[0]?.startDate).toBe("2026-04-07");
      expect(rightResizeDemands[0]!.endDate > "2026-04-16").toBe(true);
      expect(rightResizeDemands[0]?.headcount).toBe(2);
      expect(rightResizeDemands[0]?.status).toBe("PROPOSED");

      const resizeStart = await measureAllocationResizeStartGap(page, demandSelector);
      expect(resizeStart.widthGain).toBeGreaterThan(36);
      expect(resizeStart.leftEdgeGain).toBeGreaterThan(24);

      let leftResizeDemands: Array<{
        id: string;
        startDate: string;
        endDate: string;
        headcount: number;
        status: string;
      }> = [];
      await expect
        .poll(() => {
          leftResizeDemands = listScenarioDemands(scenario.projectId);
          if (leftResizeDemands.length !== 1) {
            return null;
          }

          const [demand] = leftResizeDemands;
          if (!demand || demand.id !== scenario.demandId) {
            return null;
          }

          return demand.startDate;
        }, { timeout: 15_000 })
        .not.toBe("2026-04-07");
      expect(leftResizeDemands).toHaveLength(1);
      expect(leftResizeDemands[0]?.id).toBe(scenario.demandId);
      expect(leftResizeDemands[0]!.startDate < "2026-04-07").toBe(true);
      expect(leftResizeDemands[0]?.endDate).toBe(rightResizeDemands[0]?.endDate);
      expect(leftResizeDemands[0]?.headcount).toBe(2);
      expect(leftResizeDemands[0]?.status).toBe("PROPOSED");
    } finally {
      cleanupTimelineDemandScenario(scenario.projectId, scenario.resourceId);
    }
  });

  test("resource timeline supports resizing, moving, carving, and recreating segmented allocations with persisted dates", async ({
    page,
  }) => {
    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineSegmentScenario(suffix);
    const scenarioSnapshot = readScenarioSnapshot(
      scenario.projectId,
      scenario.resourceId,
      scenario.resourceEid,
    );

    try {
      expect(scenarioSnapshot.resource?.eid).toBe(scenario.resourceEid);
      expect(scenarioSnapshot.project?.id).toBe(scenario.projectId);
      expect(scenarioSnapshot.assignments).toEqual([
        {
          id: scenario.assignmentId,
          startDate: "2026-04-06",
          endDate: "2026-04-17",
          status: "ACTIVE",
        },
      ]);

      await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, {
        waitUntil: "domcontentloaded",
      });

      const row = page
        .locator(
          `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`,
        )
        .first();
      await expect(row).toBeVisible();

      const renderedSegments = await listRenderedAllocationSegments(row, scenario.assignmentId);
      console.log(
        `[timeline-e2e] rendered segments ${JSON.stringify({
          resourceEid: scenario.resourceEid,
          assignmentId: scenario.assignmentId,
          renderedSegments,
        })}`,
      );

      const baseSegment = row.locator(
        `[data-allocation-id="${scenario.assignmentId}"][data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-10"]`,
      );
      await expect(baseSegment).toBeVisible();
      await expect(baseSegment.locator('[data-allocation-handle="start"]')).toBeVisible();
      await expect(baseSegment.locator('[data-allocation-handle="end"]')).toBeVisible();

      const baseSegmentBox = await readBoundingBox(baseSegment);
      const dayWidth = Math.round((baseSegmentBox.width + 4) / 5);
      expect(dayWidth).toBeGreaterThan(8);

      await dragLocatorBy(page, baseSegment.locator('[data-allocation-handle="start"]'), dayWidth);
      await releaseMouse(page);

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-07", endDate: "2026-04-10" },
        { startDate: "2026-04-11", endDate: "2026-04-17" },
      ]);

      const resizedSegment = row.locator(
        `[data-allocation-id="${scenario.assignmentId}"][data-allocation-segment-start="2026-04-07"][data-allocation-segment-end="2026-04-10"]`,
      );
      await expect(resizedSegment).toBeVisible();

      await dragLocatorBy(page, resizedSegment.locator('[data-allocation-interaction="body"]'), -dayWidth);
      await releaseMouse(page);

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-09" },
        { startDate: "2026-04-11", endDate: "2026-04-17" },
      ]);

      const movedSegment = row.locator(
        `[data-allocation-id="${scenario.assignmentId}"][data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-09"]`,
      );
      await expect(movedSegment).toBeVisible();

      await openAllocationContextMenuAtOffset(page, movedSegment, dayWidth * 1.5);
      const cancelButton = page.getByRole("button", { name: "Cancel" }).last();
      await expect(cancelButton).toBeVisible();
      await cancelButton.click();
      await expect(cancelButton).not.toBeVisible();

      await dragLocatorBy(page, movedSegment.locator('[data-allocation-handle="end"]'), dayWidth);
      await releaseMouse(page);

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-10" },
        { startDate: "2026-04-11", endDate: "2026-04-17" },
      ]);

      const restoredSegment = row.locator(
        `[data-allocation-id="${scenario.assignmentId}"][data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-10"]`,
      );
      await expect(restoredSegment).toBeVisible();

      await openAllocationContextMenuAtOffset(page, restoredSegment, dayWidth * 2.5);
      const carveDateInputs = page.locator('input[placeholder="dd/mm/yyyy"]');
      await expect(carveDateInputs.nth(2)).toHaveValue("08/04/2026");
      await expect(carveDateInputs.nth(3)).toHaveValue("08/04/2026");
      await page.getByRole("button", { name: "Remove Selected Range" }).click();

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-07" },
        { startDate: "2026-04-09", endDate: "2026-04-10" },
        { startDate: "2026-04-11", endDate: "2026-04-17" },
      ]);

      const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first();
      const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first();
      const nextWeekSegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first();
      await expect(leftSplit).toBeVisible();
      await expect(rightSplit).toBeVisible();
      await expect(nextWeekSegment).toBeVisible();

      const leftSplitBox = await readBoundingBox(leftSplit);
      const rightSplitBox = await readBoundingBox(rightSplit);
      const rowBox = await readBoundingBox(row);
      const gapCenterX = (leftSplitBox.x + leftSplitBox.width + rightSplitBox.x) / 2;
      const gapCenterY = rowBox.y + Math.min(rowBox.height / 2, rowBox.height - 6);

      await page.mouse.move(gapCenterX, gapCenterY);
      await page.mouse.down();
      await page.mouse.up();

      await expect(page.getByText("Assign to Project")).toBeVisible();

      await selectProjectFromCombobox(page, scenario.projectShortCode, scenario.projectName);
      await page.getByRole("button", { name: "Assign" }).click();

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-07" },
        { startDate: "2026-04-08", endDate: "2026-04-08" },
        { startDate: "2026-04-09", endDate: "2026-04-10" },
        { startDate: "2026-04-11", endDate: "2026-04-17" },
      ]);

      await expect(
        row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]').first(),
      ).toBeVisible();

      await page.reload({ waitUntil: "domcontentloaded" });
      await expect(row).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(),
      ).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-08"]').first(),
      ).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(),
      ).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(),
      ).toBeVisible();
    } finally {
      cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
    }
  });

  test("resource timeline persists multi-day carve and reverse recreation after reload", async ({
    page,
  }) => {
    test.setTimeout(60_000);

    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineSegmentScenario(suffix);

    try {
      await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, {
        waitUntil: "domcontentloaded",
      });

      const row = page
        .locator(
          `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`,
        )
        .first();
      await expect(row).toBeVisible();

      const baseSegment = row.locator(
        `[data-allocation-id="${scenario.assignmentId}"][data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-10"]`,
      );
      await expect(baseSegment).toBeVisible();

      const baseSegmentBox = await readBoundingBox(baseSegment);
      const dayWidth = Math.round((baseSegmentBox.width + 4) / 5);
      expect(dayWidth).toBeGreaterThan(8);

      await openAllocationContextMenuAtOffset(page, baseSegment, dayWidth * 2.5);
      const carveDateInputs = page.locator('input[placeholder="dd/mm/yyyy"]');
      await carveDateInputs.nth(2).fill("08/04/2026");
      await carveDateInputs.nth(3).fill("09/04/2026");
      await page.getByRole("button", { name: "Remove Selected Range" }).click();

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-07" },
        { startDate: "2026-04-10", endDate: "2026-04-17" },
      ]);

      await page.reload({ waitUntil: "domcontentloaded" });
      await expect(row).toBeVisible();

      const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first();
      const fridayBridge = row.locator('[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]').first();
      const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first();
      await expect(leftSplit).toBeVisible();
      await expect(fridayBridge).toBeVisible();
      await expect(mondaySegment).toBeVisible();

      const fridayBridgeBox = await readBoundingBox(fridayBridge);
      await dragRowSelection(
        page,
        row,
        fridayBridgeBox.x - dayWidth / 2,
        fridayBridgeBox.x - dayWidth * 1.5,
      );

      await expect(page.getByText("Assign to Project")).toBeVisible();
      await selectProjectFromCombobox(page, scenario.projectShortCode, scenario.projectName);
      await page.getByRole("button", { name: "Assign" }).click();

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-07" },
        { startDate: "2026-04-08", endDate: "2026-04-09" },
        { startDate: "2026-04-10", endDate: "2026-04-17" },
      ]);

      await page.reload({ waitUntil: "domcontentloaded" });
      await expect(row).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-08"][data-allocation-segment-end="2026-04-09"]').first(),
      ).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-10"][data-allocation-segment-end="2026-04-10"]').first(),
      ).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(),
      ).toBeVisible();
    } finally {
      cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
    }
  });

  test("resource timeline narrow split fragments keep both handles and monday context dates", async ({
    page,
  }) => {
    test.setTimeout(60_000);

    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineSegmentScenario(suffix);

    try {
      await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, {
        waitUntil: "domcontentloaded",
      });

      const row = page
        .locator(
          `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`,
        )
        .first();
      await expect(row).toBeVisible();

      const baseSegment = row.locator(
        `[data-allocation-id="${scenario.assignmentId}"][data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-10"]`,
      );
      await expect(baseSegment).toBeVisible();

      const baseSegmentBox = await readBoundingBox(baseSegment);
      const dayWidth = Math.round((baseSegmentBox.width + 4) / 5);
      expect(dayWidth).toBeGreaterThan(8);

      await openAllocationContextMenuAtOffset(page, baseSegment, dayWidth * 2.5);
      const carveDateInputs = page.locator('input[placeholder="dd/mm/yyyy"]');
      await expect(carveDateInputs.nth(2)).toHaveValue("08/04/2026");
      await expect(carveDateInputs.nth(3)).toHaveValue("08/04/2026");
      await page.getByRole("button", { name: "Remove Selected Range" }).click();

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-07" },
        { startDate: "2026-04-09", endDate: "2026-04-17" },
      ]);

      const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first();
      const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first();
      const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first();
      await expect(leftSplit).toBeVisible();
      await expect(rightSplit).toBeVisible();
      await expect(mondaySegment).toBeVisible();

      await expect(leftSplit.locator('[data-allocation-handle="start"]')).toBeVisible();
      await expect(leftSplit.locator('[data-allocation-handle="end"]')).toBeVisible();
      await expect(rightSplit.locator('[data-allocation-handle="start"]')).toBeVisible();
      await expect(rightSplit.locator('[data-allocation-handle="end"]')).toBeVisible();

      await dragLocatorBy(page, leftSplit.locator('[data-allocation-handle="start"]'), dayWidth);
      await releaseMouse(page);

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-07", endDate: "2026-04-07" },
        { startDate: "2026-04-09", endDate: "2026-04-17" },
      ]);

      const resizedRightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first();
      await dragLocatorBy(page, resizedRightSplit.locator('[data-allocation-handle="end"]'), -dayWidth);
      await releaseMouse(page);

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-07", endDate: "2026-04-07" },
        { startDate: "2026-04-09", endDate: "2026-04-09" },
        { startDate: "2026-04-11", endDate: "2026-04-17" },
      ]);

      await page.reload({ waitUntil: "domcontentloaded" });
      await expect(row).toBeVisible();

      const mondaySegmentAfterReload = row.locator(
        '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
      ).first();
      await expect(mondaySegmentAfterReload).toBeVisible();

      const mondayCarveDateInputs = page.locator('input[placeholder="dd/mm/yyyy"]');
      const mondayCancelButton = page.getByRole("button", { name: "Cancel" }).last();

      await openAllocationContextMenuAtOffset(page, mondaySegmentAfterReload, dayWidth * 0.5);
      await expect(page.getByText("Loading...")).not.toBeVisible();
      await expect(mondayCancelButton).toBeVisible();
      await expect(mondayCarveDateInputs.nth(2)).toHaveValue("13/04/2026");
      await expect(mondayCarveDateInputs.nth(3)).toHaveValue("13/04/2026");
      await mondayCancelButton.click();
      await expect(mondayCancelButton).not.toBeVisible();

      await openAllocationContextMenuAtOffset(page, mondaySegmentAfterReload, dayWidth * 4.5);
      await expect(page.getByText("Loading...")).not.toBeVisible();
      await expect(mondayCancelButton).toBeVisible();
      await expect(mondayCarveDateInputs.nth(2)).toHaveValue("17/04/2026");
      await expect(mondayCarveDateInputs.nth(3)).toHaveValue("17/04/2026");
      await mondayCancelButton.click();
      await expect(mondayCancelButton).not.toBeVisible();
    } finally {
      cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
    }
  });

  test("resource timeline can delete a two-day weekday fragment without disturbing adjacent pieces", async ({
    page,
  }) => {
    test.setTimeout(60_000);

    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineSegmentScenario(suffix);

    try {
      await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, {
        waitUntil: "domcontentloaded",
      });

      const row = page
        .locator(
          `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`,
        )
        .first();
      await expect(row).toBeVisible();

      const baseSegment = row.locator(
        `[data-allocation-id="${scenario.assignmentId}"][data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-10"]`,
      );
      await expect(baseSegment).toBeVisible();

      const baseSegmentBox = await readBoundingBox(baseSegment);
      const dayWidth = Math.round((baseSegmentBox.width + 4) / 5);
      expect(dayWidth).toBeGreaterThan(8);

      await openAllocationContextMenuAtOffset(page, baseSegment, dayWidth * 2.5);
      const carveDateInputs = page.locator('input[placeholder="dd/mm/yyyy"]');
      await carveDateInputs.nth(2).fill("08/04/2026");
      await carveDateInputs.nth(3).fill("08/04/2026");
      await page.getByRole("button", { name: "Remove Selected Range" }).click();

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-07" },
        { startDate: "2026-04-09", endDate: "2026-04-17" },
      ]);

      const leftSplit = row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first();
      const rightSplit = row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first();
      const mondaySegment = row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first();
      await expect(leftSplit).toBeVisible();
      await expect(rightSplit).toBeVisible();
      await expect(mondaySegment).toBeVisible();

      await openAllocationContextMenuAtOffset(page, leftSplit, dayWidth * 0.5);
      await carveDateInputs.nth(2).fill("06/04/2026");
      await carveDateInputs.nth(3).fill("07/04/2026");
      await page.getByRole("button", { name: "Remove Selected Range" }).click();

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-09", endDate: "2026-04-17" },
      ]);

      await expect(
        row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(),
      ).toHaveCount(0);
      await expect(rightSplit).toBeVisible();
      await expect(mondaySegment).toBeVisible();

      await page.reload({ waitUntil: "domcontentloaded" });
      await expect(row).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-07"]').first(),
      ).toHaveCount(0);
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-09"][data-allocation-segment-end="2026-04-10"]').first(),
      ).toBeVisible();
      await expect(
        row.locator('[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]').first(),
      ).toBeVisible();
    } finally {
      cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
    }
  });

  test("segmented allocations stay actionable after switching to project view and reloading", async ({
    page,
  }) => {
    test.setTimeout(60_000);

    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineSegmentScenario(suffix);

    try {
      await page.goto(`/timeline?startDate=2026-04-01&days=30&eids=${scenario.resourceEid}`, {
        waitUntil: "domcontentloaded",
      });

      const resourceRow = page
        .locator(
          `[data-testid="timeline-resource-row-canvas"][data-resource-eid="${scenario.resourceEid}"]`,
        )
        .first();
      await expect(resourceRow).toBeVisible();

      const baseSegment = resourceRow.locator(
        `[data-allocation-id="${scenario.assignmentId}"][data-allocation-segment-start="2026-04-06"][data-allocation-segment-end="2026-04-10"]`,
      );
      await expect(baseSegment).toBeVisible();

      const baseSegmentBox = await readBoundingBox(baseSegment);
      const dayWidth = Math.round((baseSegmentBox.width + 4) / 5);
      expect(dayWidth).toBeGreaterThan(8);

      await openAllocationContextMenuAtOffset(page, baseSegment, dayWidth * 2.5);
      const carveDateInputs = page.locator('input[placeholder="dd/mm/yyyy"]');
      await carveDateInputs.nth(2).fill("08/04/2026");
      await carveDateInputs.nth(3).fill("08/04/2026");
      await page.getByRole("button", { name: "Remove Selected Range" }).click();

      await waitForScenarioAssignments(scenario.projectId, [
        { startDate: "2026-04-06", endDate: "2026-04-07" },
        { startDate: "2026-04-09", endDate: "2026-04-17" },
      ]);

      const mondaySegment = resourceRow.locator(
        '[data-allocation-segment-start="2026-04-13"][data-allocation-segment-end="2026-04-17"]',
      ).first();
      await expect(mondaySegment).toBeVisible();

      const projectRowSelector =
        `[data-testid="timeline-project-resource-row-canvas"][data-project-id="${scenario.projectId}"][data-resource-id="${scenario.resourceId}"]`;
      await switchToProjectView(page, projectRowSelector);

      let mondayAssignment: { id: string; startDate: string; endDate: string } | null = null;
      await expect
        .poll(() => {
          mondayAssignment =
            listScenarioAssignments(scenario.projectId).find(
              (entry) => entry.startDate === "2026-04-09" && entry.endDate === "2026-04-17",
            ) ?? null;
          return mondayAssignment?.id ?? null;
        })
        .not.toBeNull();

      const projectRow = page.locator(projectRowSelector).first();
      await expect(projectRow).toBeVisible();

      const projectAllocation = projectRow.locator(
        `[data-timeline-entry-type="allocation"][data-allocation-id="${mondayAssignment!.id}"]`,
      );
      await expect(projectAllocation).toBeVisible();
      await openContextMenuAtCenter(page, projectAllocation);

      await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0);
      const popover = page.getByTestId("timeline-allocation-popover");
      await expect(popover).toBeVisible();
      await expect(page.getByTestId("timeline-allocation-popover-error")).toHaveCount(0);
      await expect(page.getByTestId("timeline-allocation-popover-unavailable")).toHaveCount(0);
      await expect(popover.getByText("Hours / day")).toBeVisible();
      await expect(popover.getByRole("button", { name: "Open Project Panel →" })).toBeVisible();
      await popover.getByRole("button", { name: "Cancel" }).click();

      await page.reload({ waitUntil: "domcontentloaded" });
      await switchToProjectView(page, projectRowSelector);
      await expect(page.getByText(scenario.projectName).first()).toBeVisible();

      const projectAllocationAfterReload = page
        .locator(`[data-timeline-entry-type="allocation"][data-allocation-id="${mondayAssignment!.id}"]`)
        .first();
      await expect(projectAllocationAfterReload).toBeVisible();
      await openContextMenuAtCenter(page, projectAllocationAfterReload);

      await expect(page.getByTestId("timeline-allocation-popover-loading")).toHaveCount(0);
      const popoverAfterReload = page.getByTestId("timeline-allocation-popover");
      await expect(popoverAfterReload).toBeVisible();
      await expect(page.getByTestId("timeline-allocation-popover-error")).toHaveCount(0);
      await expect(page.getByTestId("timeline-allocation-popover-unavailable")).toHaveCount(0);
      await expect(popoverAfterReload.getByText("Hours / day")).toBeVisible();
    } finally {
      cleanupTimelineSegmentScenario(scenario.projectId, scenario.resourceId);
    }
  });

  test("project view demand bars expose hover details", async ({ page }) => {
    const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
    const scenario = createTimelineDemandScenario(suffix);

    try {
      await page.goto(
        `/timeline?startDate=2026-04-01&days=31&projectIds=${scenario.projectId}`,
        { waitUntil: "domcontentloaded" },
      );
      await ensureOpenDemandVisibilityEnabled(page);
      const demandRowSelector =
        `[data-project-demand-row="true"][data-project-id="${scenario.projectId}"]`;
      const demandSelector =
        `${demandRowSelector} [data-timeline-entry-type="demand"][data-allocation-id="${scenario.demandId}"]`;

      await switchToProjectView(page, demandRowSelector);
      await expect(page.locator(demandSelector)).toBeVisible();

      const demandBar = page.locator(demandSelector);
      await demandBar.hover();

      const demandTooltip = page.getByTestId("timeline-demand-tooltip");
      await expect(demandTooltip).toBeVisible();
      await expect(demandTooltip).toContainText("E2E Open Demand");
      await expect(demandTooltip).toContainText(scenario.projectShortCode);
      await expect(demandTooltip).toContainText("Requested");
      await expect(demandTooltip).toContainText("2 seats");
      await expect(demandTooltip).toContainText("Open");
      await expect(demandTooltip).toContainText("Click for details and actions");
    } finally {
      cleanupTimelineDemandScenario(scenario.projectId, scenario.resourceId);
    }
  });
});
