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

test("speech capability detection preserves the server hydration tree", async ({ page }) => {
  const hydrationErrors: string[] = [];

  page.on("console", (message) => {
    if (message.type() === "error" && /hydration|server rendered html/i.test(message.text())) {
      hydrationErrors.push(message.text());
    }
  });

  await page.addInitScript(() => {
    Object.defineProperty(window, "SpeechRecognition", {
      configurable: true,
      value: class SpeechRecognition {}
    });
  });
  await page.goto("/", { waitUntil: "domcontentloaded" });

  await expect(page.getByRole("button", { name: "语音输入" })).toBeVisible();
  await expect(page.getByRole("button", { name: "权限批准模式" })).toBeVisible();
  expect(hydrationErrors).toEqual([]);
});

test("speech recognition failure clears the active animation and explains the error", async ({ page }) => {
  await page.addInitScript(() => {
    Object.defineProperty(window, "SpeechRecognition", {
      configurable: true,
      value: class SpeechRecognition {
        onerror: ((event: { error: string }) => void) | null = null;
        onend: (() => void) | null = null;

        start() {
          window.setTimeout(() => {
            this.onerror?.({ error: "not-allowed" });
            this.onend?.();
          }, 20);
        }

        stop() {
          this.onend?.();
        }

        abort() {}
      }
    });
  });
  await page.goto("/", { waitUntil: "domcontentloaded" });

  await page.getByRole("button", { name: "语音输入" }).click();

  await expect(page.getByRole("button", { name: "语音输入" })).toHaveAttribute("aria-pressed", "false");
  await expect(page.locator('[data-slot="voice-input-pulse"]')).toHaveCount(0);
  await expect(page.getByText("无法使用麦克风，请在浏览器地址栏允许麦克风权限后重试")).toBeVisible();
});

test("speech recognition writes final transcript into the prompt", async ({ page }) => {
  await page.addInitScript(() => {
    Object.defineProperty(window, "SpeechRecognition", {
      configurable: true,
      value: class SpeechRecognition {
        onresult: ((event: { resultIndex: number; results: Array<Array<{ transcript: string }> & { isFinal: boolean }> }) => void) | null = null;
        onend: (() => void) | null = null;

        start() {
          window.setTimeout(() => {
            const result = Object.assign([{ transcript: "检查城南泵站水位" }], { isFinal: true });
            this.onresult?.({ resultIndex: 0, results: [result] });
          }, 20);
        }

        stop() {
          this.onend?.();
        }

        abort() {}
      }
    });
  });
  await page.goto("/", { waitUntil: "domcontentloaded" });

  await page.getByRole("button", { name: "语音输入" }).click();

  await expect(page.getByPlaceholder("输入调度问题，Agent 将通过后端会话流式响应")).toHaveValue("检查城南泵站水位");
});

test("speech recognition pulse stays transparent across its loop boundary", async ({ page }) => {
  await page.addInitScript(() => {
    Object.defineProperty(window, "SpeechRecognition", {
      configurable: true,
      value: class SpeechRecognition {
        start() {}

        stop() {}

        abort() {}
      }
    });
  });
  await page.goto("/", { waitUntil: "domcontentloaded" });

  await page.getByRole("button", { name: "语音输入" }).click();

  const pulse = page.locator('[data-slot="voice-input-pulse"]');
  await expect(pulse).toBeVisible();
  const seamOpacity = await pulse.evaluate((element) => {
    const animation = element
      .getAnimations()
      .find((candidate) =>
        (candidate.effect as KeyframeEffect | null)
          ?.getKeyframes()
          .some((keyframe) => keyframe.opacity !== undefined)
      );
    if (!animation?.effect) {
      throw new Error("Voice input pulse opacity animation was not found");
    }

    animation.pause();
    const duration = Number(animation.effect.getTiming().duration);
    animation.currentTime = duration - 1;
    const before = Number(window.getComputedStyle(element).opacity);
    animation.currentTime = duration + 1;
    const after = Number(window.getComputedStyle(element).opacity);

    return { before, after };
  });

  expect(seamOpacity.before).toBeLessThan(0.02);
  expect(seamOpacity.after).toBeLessThan(0.02);
});
