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

const rawPrimaryThreadId = process.env.FRONTEND_E2E_THREAD_ID?.trim();

function envThread(name: string) {
  return process.env[name]?.trim() ?? undefined;
}

export const PRIMARY_THREAD_ID = rawPrimaryThreadId ?? undefined;
export const THREAD_FOR_WELCOME = PRIMARY_THREAD_ID;
export const THREAD_WITH_HISTORY = PRIMARY_THREAD_ID;
export const THREAD_WITH_MARKDOWN = envThread(
  "FRONTEND_E2E_MARKDOWN_THREAD_ID",
);
export const THREAD_WITH_TODOS = envThread("FRONTEND_E2E_TODOS_THREAD_ID");
export const THREAD_WITH_ARTIFACTS = envThread(
  "FRONTEND_E2E_ARTIFACTS_THREAD_ID",
);
export const THREAD_WITH_IMAGE_ARTIFACT = envThread(
  "FRONTEND_E2E_IMAGE_ARTIFACT_THREAD_ID",
);
export const THREAD_WITH_HTML_ARTIFACT = envThread(
  "FRONTEND_E2E_HTML_ARTIFACT_THREAD_ID",
);
export const THREAD_WITH_REFERENCE_FIXTURE =
  THREAD_WITH_ARTIFACTS ?? PRIMARY_THREAD_ID;
export const THREAD_WITH_STALE_REFERENCE =
  THREAD_WITH_ARTIFACTS ?? PRIMARY_THREAD_ID;

export function skipIfMissingThread(
  testInfo: TestInfo,
  threadId: string | undefined,
  label: string,
) {
  testInfo.skip(!threadId, `未配置 ${label}。`);
}

export function buildChatUrl({
  pathThreadId,
  isChatting,
  threadId,
}: {
  pathThreadId?: string;
  isChatting: boolean;
  threadId: string;
}) {
  const resolvedThreadId = threadId ?? pathThreadId;
  if (!pathThreadId && !resolvedThreadId) {
    throw new Error("threadId is required for /workspace/chats/new routes.");
  }

  const query = new URLSearchParams();
  query.set("is_chatting", String(isChatting));
  if (resolvedThreadId) {
    query.set("thread_id", resolvedThreadId);
  }

  const basePath = pathThreadId
    ? `/workspace/chats/${pathThreadId}`
    : "/workspace/chats/new";
  return `${basePath}?${query.toString()}`;
}

export function invalidNewChatUrl() {
  const query = new URLSearchParams();
  query.set("is_chatting", "false");
  return `/workspace/chats/new?${query.toString()}`;
}

export function newChatEntry(threadId: string) {
  return buildChatUrl({
    isChatting: false,
    threadId,
  });
}

export function reuseThreadWelcomeEntry(threadId: string) {
  return buildChatUrl({
    isChatting: false,
    threadId,
  });
}

export function reuseThreadChatEntry(threadId: string) {
  return buildChatUrl({
    pathThreadId: threadId,
    isChatting: true,
    threadId,
  });
}

export async function openChat(
  page: Page,
  url: string,
  options?: { expectInput?: boolean },
) {
  await page.goto(url);
  if (options?.expectInput ?? true) {
    await expect(page.locator("textarea[name='message']")).toBeVisible();
  }
}

export async function setTheme(page: Page, theme: "light" | "dark") {
  await page.evaluate((nextTheme) => {
    const root = document.documentElement;
    root.classList.remove("light", "dark");
    root.classList.add(nextTheme);
    root.style.colorScheme = nextTheme;
  }, theme);
}

export async function expandComposer(page: Page) {
  const expander = page.locator("div.absolute.inset-0.z-1.cursor-text");
  if ((await expander.count()) > 0) {
    await expander.first().click();
  }
}

export async function openReferencePicker(page: Page, value = "@") {
  const textarea = page.locator("textarea[name='message']");
  const panel = page.getByTestId("mention-candidate-panel").first();
  await textarea.focus();

  for (let attempt = 0; attempt < 3; attempt += 1) {
    await textarea.type(value);
    const visible = await panel.isVisible().catch(() => false);
    if (visible) {
      await page.waitForTimeout(100);
      return { textarea, panel };
    }
    // Reset the transient token and retry when dropdown opening lags in fast loops.
    await textarea.press("Backspace");
    await page.waitForTimeout(100);
  }

  await expect(panel).toBeVisible();
  return { textarea, panel };
}

export async function collectMentionCandidateKeys(page: Page) {
  const items = page.getByTestId("mention-candidate-item");
  const count = await items.count();
  const keys: string[] = [];
  for (let index = 0; index < count; index += 1) {
    const key = await items.nth(index).getAttribute("data-candidate-key");
    if (key) {
      keys.push(key);
    }
  }
  return keys;
}

export function mentionCandidateByKey(page: Page, key: string) {
  return page.locator(
    `[data-testid="mention-candidate-item"][data-candidate-key=${JSON.stringify(key)}]`,
  );
}

export function toastByText(page: Page, text: string) {
  return page.locator("[data-sonner-toast]").filter({ hasText: text }).first();
}

export async function rewriteFirstReferenceAsArtifact(
  page: Page,
  artifactPath: string,
) {
  const textarea = page.locator("textarea[name='message']");
  const rewritten = await textarea.evaluate((element, nextArtifactPath) => {
    const fiberKey = Object.keys(element).find((key) =>
      key.startsWith("__reactFiber$"),
    );
    if (!fiberKey) {
      return false;
    }

    let fiber = (element as unknown as Record<string, unknown>)[fiberKey] as
      | {
          return?: unknown;
          memoizedState?: unknown;
          elementType?: { name?: string };
        }
      | undefined;

    while (fiber) {
      if (fiber.elementType?.name === "InputBox") {
        break;
      }
      fiber = fiber.return as typeof fiber;
    }

    let hook = fiber?.memoizedState as
      | {
          memoizedState?: unknown;
          next?: unknown;
          queue?: { dispatch?: (value: unknown) => void };
        }
      | undefined;

    while (hook) {
      const state = hook.memoizedState;
      const dispatch = hook.queue?.dispatch;
      if (
        Array.isArray(state) &&
        state.some(
          (item) =>
            item &&
            typeof item === "object" &&
            "ref_kind" in item &&
            (item as { ref_kind?: string }).ref_kind === "mention",
        ) &&
        dispatch
      ) {
        dispatch(
          state.map((item, index) =>
            index === 0 && item && typeof item === "object"
              ? {
                  ...item,
                  path: nextArtifactPath,
                  ref_source: "artifact",
                }
              : item,
          ),
        );
        return true;
      }
      hook = hook.next as typeof hook;
    }

    return false;
  }, artifactPath);

  expect(rewritten).toBe(true);
}

export async function stubReferenceFixtures(
  page: Page,
  options: {
    threadId: string;
    artifactPaths?: string[];
    uploadFiles?: Array<{
      filename: string;
      size?: number;
      virtual_path: string;
      path?: string;
      artifact_url?: string;
    }>;
  },
) {
  const { threadId, artifactPaths = [], uploadFiles = [] } = options;

  await page.route(
    new RegExp(`/api/langgraph/threads/${threadId}/state(?:/[^?]+)?(\\?.*)?$`),
    async (route) => {
      const response = await route.fetch();
      const json = await response.json();
      await route.fulfill({
        response,
        json: {
          ...json,
          values: {
            ...json.values,
            artifacts: artifactPaths,
          },
        },
      });
    },
  );

  await page.route(
    new RegExp(`/api/langgraph/threads/${threadId}/history(\\?.*)?$`),
    async (route) => {
      const response = await route.fetch();
      const json = await response.json();
      const history = Array.isArray(json) ? json : [];
      await route.fulfill({
        response,
        json: history.map((entry) => ({
          ...entry,
          values: {
            ...entry.values,
            artifacts: artifactPaths,
          },
        })),
      });
    },
  );

  await page.route(
    new RegExp(`/api/threads/${threadId}/uploads/list(\\?.*)?$`),
    async (route) => {
      const response = await route.fetch();
      const json = await response.json().catch(() => ({ files: [], count: 0 }));
      const files = uploadFiles.map((file) => ({
        filename: file.filename,
        size: file.size ?? 128,
        path: file.path ?? file.virtual_path,
        virtual_path: file.virtual_path,
        artifact_url:
          file.artifact_url ??
          `/api/threads/${threadId}/artifacts${file.virtual_path}`,
      }));
      await route.fulfill({
        response,
        json: {
          ...json,
          files,
          count: files.length,
        },
      });
    },
  );
}

export async function sendMessage(page: Page, text: string) {
  const textarea = page.locator("textarea[name='message']");
  const submit = page.locator("button[aria-label='Submit']");
  await textarea.click();
  await textarea.fill(text);
  await submit.click();
}

export async function waitForMessageListReady(
  page: Page,
  options?: { requireMessages?: boolean; minMessages?: number },
) {
  const { requireMessages = false, minMessages = 1 } = options ?? {};
  await expect(page.getByRole("main").first()).toBeVisible();
  if (requireMessages) {
    await expect
      .poll(async () => await page.locator(".is-user, .is-assistant").count(), {
        timeout: 30_000,
      })
      .toBeGreaterThan(minMessages - 1);
  }
}

export async function waitForAnyMessages(page: Page, timeoutMs = 15_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const count = await page.locator(".is-user, .is-assistant").count();
    if (count > 0) {
      return count;
    }
    await page.waitForTimeout(300);
  }
  return 0;
}
