import { expect, test, type Locator } from "@playwright/test";

type BoundingBox = {
  x: number;
  y: number;
  width: number;
  height: number;
};

async function waitForAnimations(locator: Locator) {
  await locator.evaluate(async (element) => {
    const animations = element
      .getAnimations({ subtree: true })
      .filter((animation) => animation.effect?.getTiming().iterations !== Infinity);
    await Promise.all(
      animations.map((animation) => animation.finished.catch(() => undefined))
    );
  });
}

test("desktop Agent layers floating controls inside one acrylic panel", async ({ page }) => {
  await page.goto("/", { waitUntil: "domcontentloaded" });

  const topBar = page.locator("header.acrylic-navigation");
  const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
  const agentHeader = agentPanel.locator(".agent-panel-integrated-header");
  const agentConversation = agentPanel.locator(".agent-panel-conversation-canvas");
  const agentConversationScroll = agentPanel.locator(".agent-conversation-scroll");
  const agentComposer = agentPanel.locator(".agent-panel-composer");

  await expect(topBar).toBeVisible();
  await expect(agentPanel).toBeVisible();
  await expect(agentHeader).toBeVisible();
  await expect(agentComposer).toBeVisible();
  await waitForAnimations(agentPanel);

  await expectAcrylicSurface(topBar);
  await expectAcrylicSurface(agentPanel, ".agent-panel-composer");
  await expectIntegratedHeader(agentHeader);
  await expectContextualAcrylicSurface(agentComposer, {
    backdropFilter: "blur(",
    maximumAlpha: 0.9,
    minimumAlpha: 0.84
  });
  await expectLightweightMaterialContext(agentConversation);
  await expectUnfilteredInternalSurfaces(agentComposer.locator(".agent-panel-control"));
  await expect(agentComposer).toHaveCSS("background-color", "rgba(255, 255, 255, 0.86)");
  await expect(agentComposer).toHaveCSS("border-radius", "16px");
  await expect(agentComposer).toHaveCSS("border-top-width", "0px");
  await expect(
    agentPanel.getByText("直接说出你想调查的问题，我会整理监测与空间证据，并将分析结果带回地图。")
  ).toHaveCSS("color", "rgb(51, 65, 85)");
  await expect(agentPanel.getByText("水力瓶颈诊断")).toHaveCSS("color", "rgb(30, 41, 59)");
  await expect(agentPanel.getByText("水力瓶颈诊断")).toHaveCSS("font-size", "14px");

  const panelBox = await agentPanel.boundingBox();
  const conversationBox = await agentConversation.boundingBox();
  const scrollBox = await agentConversationScroll.boundingBox();
  const composerBox = await agentComposer.boundingBox();
  expect(panelBox).not.toBeNull();
  expect(conversationBox).not.toBeNull();
  expect(scrollBox).not.toBeNull();
  expect(composerBox).not.toBeNull();
  expect(
    Math.abs(conversationBox!.y + conversationBox!.height - (panelBox!.y + panelBox!.height - 4))
  ).toBeLessThanOrEqual(1);
  expect(
    Math.abs(scrollBox!.y + scrollBox!.height - (conversationBox!.y + conversationBox!.height))
  ).toBeLessThanOrEqual(1);
  expect(composerBox!.y).toBeLessThan(conversationBox!.y + conversationBox!.height);
  expect(composerBox!.x - panelBox!.x).toBeGreaterThanOrEqual(21);
  expect(
    Math.abs(
      composerBox!.x -
        panelBox!.x -
        (panelBox!.x + panelBox!.width - composerBox!.x - composerBox!.width)
    )
  ).toBeLessThanOrEqual(1);
  await expect(agentConversationScroll).toHaveCSS("scrollbar-gutter", "stable both-edges");

  const beforeFocus = await getFloatingSurfaceStyles(agentHeader, agentComposer);
  await page.getByPlaceholder("输入调度问题，Agent 将通过后端会话流式响应").focus();
  await expect
    .poll(() => getFloatingSurfaceStyles(agentHeader, agentComposer))
    .toEqual(beforeFocus);
});

async function expectIntegratedHeader(header: Locator) {
  const composition = await header.evaluate((element) => {
    const style = getComputedStyle(element);

    return {
      backdropFilter: style.backdropFilter,
      backgroundColor: style.backgroundColor,
      borderTopWidth: style.borderTopWidth,
      borderRadius: style.borderRadius
    };
  });

  expect(composition.backdropFilter).toBe("none");
  expect(getAlpha(composition.backgroundColor)).toBe(0);
  expect(composition.borderTopWidth).toBe("0px");
  expect(composition.borderRadius).toBe("0px");
}

test("desktop Agent header expands into one attached acrylic history surface", async ({
  page
}) => {
  await page.goto("/", { waitUntil: "domcontentloaded" });

  const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
  const agentHeader = agentPanel.locator(".agent-panel-integrated-header");
  const agentConversation = agentPanel.locator(".agent-panel-conversation-canvas");
  const historyButton = agentPanel.getByRole("button", {
    name: "打开 Agent 历史记录"
  });
  const headerBefore = await agentHeader.boundingBox();
  const conversationBefore = await agentConversation.boundingBox();
  const historyButtonPresentationBefore = await getSurfacePresentation(historyButton);
  const agentPanelFrameBefore = await expectAcrylicOutline(agentPanel);

  await historyButton.click();

  const history = agentPanel.getByRole("region", { name: "Agent 历史记录" });
  await expect(history).toBeVisible();
  await waitForAnimations(history);
  await expect(historyButton).toHaveAttribute("aria-expanded", "true");
  await page.mouse.move(720, 320);
  await expectAttachedHistoryExtension(history);
  await expectTransparentExpandedHeader(agentHeader, agentPanel);
  await expectUnifiedHistoryStructure(history);
  await expect.poll(() => getAcrylicFrame(agentPanel)).toEqual(agentPanelFrameBefore);
  await expect
    .poll(() => getSurfacePresentation(historyButton))
    .toEqual(historyButtonPresentationBefore);

  const sessionTrigger = history.locator(".agent-history-session-trigger").first();
  const sessionTriggerBox = await sessionTrigger.boundingBox();
  const historyHeadingBox = await history.getByText("历史记录", { exact: true }).boundingBox();
  expect(sessionTriggerBox).not.toBeNull();
  expect(historyHeadingBox).not.toBeNull();
  await page.mouse.move(
    sessionTriggerBox!.x + sessionTriggerBox!.width / 2,
    sessionTriggerBox!.y + sessionTriggerBox!.height / 2
  );
  await page.mouse.down();
  await expect(sessionTrigger).toHaveCSS("outline-style", "none");
  await page.mouse.move(
    historyHeadingBox!.x + historyHeadingBox!.width / 2,
    historyHeadingBox!.y + historyHeadingBox!.height / 2
  );
  await page.mouse.up();

  const headerBox = await agentHeader.boundingBox();
  const agentPanelBox = await agentPanel.boundingBox();
  const historyBox = await history.boundingBox();
  const conversationAfter = await agentConversation.boundingBox();
  expect(headerBefore).not.toBeNull();
  expect(headerBox).not.toBeNull();
  expect(agentPanelBox).not.toBeNull();
  expect(historyBox).not.toBeNull();
  expect(conversationBefore).not.toBeNull();
  expect(conversationAfter).not.toBeNull();
  expectRoundedValue(headerBox!.y, headerBefore!.y);
  expect(headerBox!.height).toBeGreaterThan(headerBefore!.height);
  expectRoundedValue(historyBox!.y, headerBefore!.y + headerBefore!.height);
  expectRoundedValue(headerBox!.x, agentPanelBox!.x + 1);
  expectRoundedValue(headerBox!.y, agentPanelBox!.y + 1);
  expectRoundedValue(headerBox!.width, agentPanelBox!.width - 2);
  expectRoundedValue(historyBox!.x, headerBox!.x);
  expectRoundedValue(historyBox!.width, headerBox!.width);
  expectRoundedBox(conversationAfter!, conversationBefore!);

  await history.getByRole("button", { name: "重命名对话" }).first().click();
  const renameInput = history.locator("input").first();
  const editingItem = renameInput.locator(
    "xpath=ancestor::*[contains(@class, 'agent-history-item')][1]"
  );
  await expect(renameInput).toBeFocused();
  await expect(renameInput).toHaveCSS("box-shadow", "none");
  await expect(renameInput).toHaveCSS("border-color", "rgb(203, 213, 225)");
  await expect(editingItem).toHaveCSS("border-color", "rgba(0, 0, 0, 0)");
  await history.getByRole("button", { name: "取消重命名" }).click();

  await page.keyboard.press("Escape");
  await expect(history).toBeHidden();
  await expect(historyButton).toBeFocused();
  await expect(historyButton).toHaveAttribute("aria-expanded", "false");
  await expect.poll(() => getAcrylicFrame(agentPanel)).toEqual(agentPanelFrameBefore);

  await historyButton.click();
  await expect(history).toBeVisible();
  await page.mouse.click(720, 320);
  await expect(history).toBeHidden();
});

test("mobile Agent mirrors the desktop floating history surface", async ({
  page
}) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto("/", { waitUntil: "domcontentloaded" });
  await page.getByRole("button", { name: "打开 Agent 面板" }).click();

  const agentPanel = page
    .locator('aside[aria-label="Agent 命令面板"]')
    .filter({ visible: true });
  await expect(agentPanel).toBeVisible();
  await waitForAnimations(agentPanel);
  const mobileSheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
  const sheetHandle = mobileSheet.locator(".mobile-workbench-sheet-handle");
  const agentHeader = agentPanel.locator(".agent-panel-header");
  const agentConversation = agentPanel.getByRole("log");
  const historyButton = agentPanel.getByRole("button", {
    name: "打开 Agent 历史记录"
  });
  const headerBefore = await agentHeader.boundingBox();
  const conversationBefore = await agentConversation.boundingBox();
  const historyButtonPresentationBefore = await getSurfacePresentation(historyButton);
  const mobileSheetFrameBefore = await expectAcrylicOutline(mobileSheet);

  await historyButton.click();

  const history = agentPanel.getByRole("region", { name: "Agent 历史记录" });
  await expect(history).toBeVisible();
  await waitForAnimations(history);
  await page.mouse.move(10, 100);
  await expectAttachedHistoryExtension(history);
  await expectTransparentExpandedHeader(agentHeader, mobileSheet);
  await expectUnifiedHistoryStructure(history);
  await expect.poll(() => getAcrylicFrame(mobileSheet)).toEqual(mobileSheetFrameBefore);
  await expect(agentPanel).toHaveCSS("box-shadow", "none");
  await expect
    .poll(() => getSurfacePresentation(historyButton))
    .toEqual(historyButtonPresentationBefore);
  await expect(sheetHandle).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");

  const panelBox = await agentPanel.boundingBox();
  const headerAfter = await agentHeader.boundingBox();
  const historyBox = await history.boundingBox();
  const conversationAfter = await agentConversation.boundingBox();
  expect(panelBox).not.toBeNull();
  expect(headerBefore).not.toBeNull();
  expect(headerAfter).not.toBeNull();
  expect(historyBox).not.toBeNull();
  expect(conversationBefore).not.toBeNull();
  expect(conversationAfter).not.toBeNull();
  expectRoundedValue(headerAfter!.y, headerBefore!.y);
  expectRoundedValue(headerAfter!.x, panelBox!.x);
  expectRoundedValue(headerAfter!.y, panelBox!.y - 28);
  expectRoundedValue(headerAfter!.width, panelBox!.width);
  expect(headerAfter!.height).toBeGreaterThan(headerBefore!.height);
  expectRoundedValue(historyBox!.y, headerBefore!.y + headerBefore!.height);
  expectRoundedValue(historyBox!.width, headerAfter!.width);
  expect(headerAfter!.height).toBeLessThan(panelBox!.height);
  expectRoundedBox(conversationAfter!, conversationBefore!);
  await expect(agentConversation).toHaveClass(/agent-panel-conversation-canvas/);
  await expect(agentPanel.locator(".agent-panel-composer")).toBeVisible();

  await page.keyboard.press("Escape");
  await expect(history).toBeHidden();
  await expect(historyButton).toBeFocused();
});

test("Agent floating acrylic respects forced color mode", async ({ page }) => {
  await page.emulateMedia({ forcedColors: "active" });
  await page.goto("/", { waitUntil: "domcontentloaded" });

  const composer = page.locator(".agent-panel-composer");
  await expect(composer).toBeVisible();
  await expect(composer).toHaveCSS("backdrop-filter", "none");
  await expect(composer).toHaveCSS("background-image", "none");
  await expect(composer).toHaveCSS("box-shadow", "none");
});

async function expectAcrylicSurface(shell: Locator, allowedFilteredDescendant?: string) {
  const composition = await shell.evaluate((element, allowedSelector) => {
    const style = getComputedStyle(element);
    const filteredDescendants: string[] = [];

    for (const descendant of element.querySelectorAll("*")) {
      if (
        getComputedStyle(descendant).backdropFilter !== "none" &&
        !(allowedSelector && descendant.matches(allowedSelector))
      ) {
        filteredDescendants.push(descendant.className);
      }
    }

    return {
      backdropFilter: style.backdropFilter,
      backgroundColor: style.backgroundColor,
      filteredDescendants,
      transform: style.transform,
      willChange: style.willChange
    };
  }, allowedFilteredDescendant);

  expect(composition.backdropFilter).toContain("blur(");
  expect(getAlpha(composition.backgroundColor)).toBeLessThan(1);
  expect(composition.filteredDescendants).toEqual([]);
  expect(composition.transform).toBe("none");
  expect(composition.willChange).toBe("auto");
}

async function expectContextualAcrylicSurface(
  context: Locator,
  {
    backdropFilter = "none",
    maximumAlpha = 0.7,
    minimumAlpha = 0.35
  }: {
    backdropFilter?: string;
    maximumAlpha?: number;
    minimumAlpha?: number;
  } = {}
) {
  const composition = await context.evaluate((element) => {
    const style = getComputedStyle(element);

    return {
      backdropFilter: style.backdropFilter,
      backgroundColor: style.backgroundColor,
      backgroundImage: style.backgroundImage,
      boxShadow: style.boxShadow
    };
  });

  if (backdropFilter === "none") {
    expect(composition.backdropFilter).toBe("none");
  } else {
    expect(composition.backdropFilter).toContain(backdropFilter);
  }
  expect(getAlpha(composition.backgroundColor)).toBeGreaterThan(minimumAlpha);
  expect(getAlpha(composition.backgroundColor)).toBeLessThan(maximumAlpha);
  expect(composition.backgroundImage).not.toBe("none");
  expect(composition.boxShadow).not.toBe("none");
}

async function expectAttachedHistoryExtension(history: Locator) {
  const presentation = await history.evaluate((element) => {
    const style = getComputedStyle(element);

    return {
      backdropFilter: style.backdropFilter,
      backgroundColor: style.backgroundColor,
      backgroundImage: style.backgroundImage,
      borderBottomLeftRadius: style.borderBottomLeftRadius,
      borderBottomRightRadius: style.borderBottomRightRadius,
      borderBottomWidth: style.borderBottomWidth,
      borderLeftWidth: style.borderLeftWidth,
      borderRightWidth: style.borderRightWidth,
      borderTopWidth: style.borderTopWidth,
      borderTopLeftRadius: style.borderTopLeftRadius,
      borderTopRightRadius: style.borderTopRightRadius,
      boxShadow: style.boxShadow
    };
  });

  expect(presentation.backdropFilter).toContain("blur(");
  expect(getAlpha(presentation.backgroundColor)).toBeGreaterThanOrEqual(0.78);
  expect(getAlpha(presentation.backgroundColor)).toBeLessThanOrEqual(0.9);
  expect(presentation.backgroundImage).not.toBe("none");
  expect(presentation.borderBottomWidth).toBe("0px");
  expect(presentation.borderLeftWidth).toBe("0px");
  expect(presentation.borderRightWidth).toBe("0px");
  expect(presentation.borderTopWidth).toBe("0px");
  expect(presentation.borderTopLeftRadius).toBe("0px");
  expect(presentation.borderTopRightRadius).toBe("0px");
  expect(presentation.borderBottomLeftRadius).toBe("16px");
  expect(presentation.borderBottomRightRadius).toBe("16px");
  expect(presentation.boxShadow).toBe("none");
}

async function getAcrylicFrame(surface: Locator) {
  return surface.evaluate((element) => {
    const style = getComputedStyle(element);

    return {
      borderBottomColor: style.borderBottomColor,
      borderLeftColor: style.borderLeftColor,
      borderRightColor: style.borderRightColor,
      borderTopColor: style.borderTopColor,
      boxShadow: style.boxShadow
    };
  });
}

async function expectAcrylicOutline(surface: Locator) {
  const frame = await getAcrylicFrame(surface);

  expect(frame.borderTopColor).not.toBe("rgba(0, 0, 0, 0)");
  expect(frame.borderRightColor).not.toBe("rgba(0, 0, 0, 0)");
  expect(frame.borderBottomColor).not.toBe("rgba(0, 0, 0, 0)");
  expect(frame.borderLeftColor).not.toBe("rgba(0, 0, 0, 0)");
  expect(frame.boxShadow).toContain("0px 1px 0px 0px inset");
  expect(frame.boxShadow).toContain("0px 0px 0px 1px");

  return frame;
}

async function expectUnifiedHistoryStructure(history: Locator) {
  const structure = await history.evaluate((element) => {
    const paragraphs = Array.from(element.querySelectorAll("p"));
    const heading = paragraphs.find((paragraph) => paragraph.textContent?.trim() === "历史记录");
    const count = paragraphs.find((paragraph) =>
      /^(?:\d+ 个会话|暂无历史会话)$/.test(paragraph.textContent?.trim() ?? "")
    );

    return {
      countTagName: count?.tagName ?? null,
      headingTagName: heading?.tagName ?? null,
      siblings: Boolean(heading && count && heading.parentElement === count.parentElement)
    };
  });

  expect(structure).toEqual({
    countTagName: "P",
    headingTagName: "P",
    siblings: true
  });
  await expect(history.getByRole("button", { name: "重命名对话" }).first()).toBeVisible();
  await expect(history.getByRole("button", { name: "删除对话" }).first()).toBeVisible();
}

async function expectTransparentExpandedHeader(header: Locator, agentSurface: Locator) {
  const [presentation, agentOutlineColor] = await Promise.all([
    header.evaluate((element) => {
      const style = getComputedStyle(element);

      return {
        backgroundColor: style.backgroundColor,
        borderBottomColor: style.borderBottomColor,
        borderBottomLeftRadius: style.borderBottomLeftRadius,
        borderBottomRightRadius: style.borderBottomRightRadius,
        borderBottomWidth: style.borderBottomWidth,
        borderLeftWidth: style.borderLeftWidth,
        borderRightWidth: style.borderRightWidth,
        borderTopWidth: style.borderTopWidth,
        boxShadow: style.boxShadow
      };
    }),
    agentSurface.evaluate((element) =>
      getComputedStyle(element).getPropertyValue("--acrylic-outline").trim()
    )
  ]);

  expect(getAlpha(presentation.backgroundColor)).toBe(0);
  expect(presentation.borderBottomColor).toBe(agentOutlineColor);
  expect(presentation.borderBottomLeftRadius).toBe("16px");
  expect(presentation.borderBottomRightRadius).toBe("16px");
  expect(presentation.borderBottomWidth).toBe("1px");
  expect(presentation.borderLeftWidth).toBe("0px");
  expect(presentation.borderRightWidth).toBe("0px");
  expect(presentation.borderTopWidth).toBe("0px");
  expect(presentation.boxShadow).toContain("0px 12px 12px -12px");
}

async function getSurfacePresentation(surface: Locator) {
  return surface.evaluate((element) => {
    const style = getComputedStyle(element);

    return {
      backdropFilter: style.backdropFilter,
      backgroundColor: style.backgroundColor,
      backgroundImage: style.backgroundImage,
      borderColor: style.borderColor,
      borderRadius: style.borderRadius,
      boxShadow: style.boxShadow,
      color: style.color
    };
  });
}

function getAlpha(color: string) {
  const match = color.match(/^rgba\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\)$/);

  return match ? Number(match[1]) : 1;
}

async function expectLightweightMaterialContext(context: Locator) {
  const style = await context.evaluate((element) => {
    const computedStyle = getComputedStyle(element);

    return {
      backdropFilter: computedStyle.backdropFilter,
      backgroundColor: computedStyle.backgroundColor,
      webkitBackdropFilter: computedStyle.getPropertyValue("-webkit-backdrop-filter")
    };
  });

  expect(style.backdropFilter).toBe("none");
  expect(getAlpha(style.backgroundColor)).toBeGreaterThan(0);
  expect(getAlpha(style.backgroundColor)).toBeLessThanOrEqual(0.3);
  expect(["", "none"]).toContain(style.webkitBackdropFilter);
}

async function expectUnfilteredInternalSurfaces(surfaces: Locator) {
  const surfaceStyles = await surfaces.evaluateAll((elements) =>
    elements.map((element) => {
      const style = getComputedStyle(element);

      return {
        backdropFilter: style.backdropFilter,
        className: element.className,
        webkitBackdropFilter: style.getPropertyValue("-webkit-backdrop-filter")
      };
    })
  );

  expect(surfaceStyles.length).toBeGreaterThan(0);
  for (const style of surfaceStyles) {
    expect(style.backdropFilter).toBe("none");
    expect(["", "none"]).toContain(style.webkitBackdropFilter);
    expect(String(style.className)).not.toContain("acrylic-");
  }
}

function expectRoundedBox(actual: BoundingBox, expected: BoundingBox) {
  expect(Math.round(actual.x)).toBe(Math.round(expected.x));
  expect(Math.round(actual.y)).toBe(Math.round(expected.y));
  expect(Math.round(actual.width)).toBe(Math.round(expected.width));
  expect(Math.round(actual.height)).toBe(Math.round(expected.height));
}

function expectRoundedValue(actual: number, expected: number) {
  expect(Math.abs(Math.round(actual) - Math.round(expected))).toBeLessThanOrEqual(1);
}

async function getFloatingSurfaceStyles(header: Locator, composer: Locator) {
  const control = composer.locator(".agent-panel-control");
  const [headerStyles, composerStyles, controlStyles] = await Promise.all([
    header.evaluate((element) => {
      const style = getComputedStyle(element);

      return {
        shadow: style.boxShadow,
        transform: style.transform
      };
    }),
    composer.evaluate((element) => {
      const style = getComputedStyle(element);

      return {
        shadow: style.boxShadow,
        transform: style.transform
      };
    }),
    control.evaluate((element) => {
      const style = getComputedStyle(element);

      return {
        borderColor: style.borderColor,
        boxShadow: style.boxShadow
      };
    })
  ]);

  return {
    composerShadow: composerStyles.shadow,
    composerTransform: composerStyles.transform,
    controlBorderColor: controlStyles.borderColor,
    controlBoxShadow: controlStyles.boxShadow,
    headerShadow: headerStyles.shadow,
    headerTransform: headerStyles.transform
  };
}
