import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import zlib from "node:zlib";
import { test as base, expect, type Page } from "@playwright/test";

// ---------------------------------------------------------------------------
// login() — fill the login form and submit (for tests that need fresh login)
// ---------------------------------------------------------------------------
export async function login(page: Page, username = "admin", password = "admin") {
  await page.goto("/login");
  await page.getByLabel("Username").fill(username);
  await page.getByLabel("Password").fill(password);
  await page.getByRole("button", { name: /login/i }).click();
  await page.waitForURL("/", { timeout: 15_000 });
}

// ---------------------------------------------------------------------------
// createTestImageFile() — create a small test PNG on disk and return its path
// ---------------------------------------------------------------------------
let _testImagePath: string | null = null;

export function getTestImagePath(): string {
  if (_testImagePath && fs.existsSync(_testImagePath)) return _testImagePath;

  const dir = path.join(process.cwd(), "test-results");
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

  _testImagePath = path.join(dir, "test-image.png");

  // Re-use an existing file (e.g. pre-created before the test run)
  if (fs.existsSync(_testImagePath)) return _testImagePath;

  try {
    const script = [
      "const sharp = require('sharp');",
      `sharp({create:{width:100,height:100,channels:4,background:{r:255,g:0,b:0,alpha:1}}}).png().toFile(${JSON.stringify(_testImagePath)})`,
    ].join(" ");
    execFileSync("node", ["-e", script], {
      cwd: process.cwd(),
      timeout: 5000,
      stdio: ["pipe", "pipe", "pipe"],
    });
  } catch {
    // Fallback: build a valid 100x100 RGBA PNG without sharp
    // zlib imported at top of file
    const width = 100;
    const height = 100;
    const raw = Buffer.alloc((1 + width * 4) * height);
    for (let y = 0; y < height; y++) {
      const off = y * (1 + width * 4);
      raw[off] = 0; // filter: none
      for (let x = 0; x < width; x++) {
        const px = off + 1 + x * 4;
        raw[px] = 255; // R
        raw[px + 3] = 255; // A
      }
    }
    const deflated = zlib.deflateSync(raw);

    const crc32 = (buf: Buffer) => {
      let c = 0xffffffff;
      const t = new Int32Array(256);
      for (let i = 0; i < 256; i++) {
        let v = i;
        for (let j = 0; j < 8; j++) v = v & 1 ? 0xedb88320 ^ (v >>> 1) : v >>> 1;
        t[i] = v;
      }
      for (let i = 0; i < buf.length; i++) c = t[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
      return (c ^ 0xffffffff) >>> 0;
    };

    const chunk = (type: string, data: Buffer) => {
      const tb = Buffer.from(type);
      const len = Buffer.alloc(4);
      len.writeUInt32BE(data.length);
      const crcBuf = Buffer.alloc(4);
      crcBuf.writeUInt32BE(crc32(Buffer.concat([tb, data])));
      return Buffer.concat([len, tb, data, crcBuf]);
    };

    const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
    const ihdr = Buffer.alloc(13);
    ihdr.writeUInt32BE(width, 0);
    ihdr.writeUInt32BE(height, 4);
    ihdr[8] = 8; // bit depth
    ihdr[9] = 6; // RGBA
    fs.writeFileSync(
      _testImagePath,
      Buffer.concat([
        sig,
        chunk("IHDR", ihdr),
        chunk("IDAT", deflated),
        chunk("IEND", Buffer.alloc(0)),
      ]),
    );
  }

  return _testImagePath;
}

// ---------------------------------------------------------------------------
// getTestHeicPath() — return a small HEIC test image (from fixtures)
// ---------------------------------------------------------------------------
export function getTestHeicPath(): string {
  return path.join(process.cwd(), "tests", "fixtures", "image", "valid", "test-200x150.heic");
}

// ---------------------------------------------------------------------------
// uploadTestImage() — upload a test image via the file chooser on a tool page
// ---------------------------------------------------------------------------
export async function uploadTestImage(page: Page): Promise<void> {
  const testImagePath = getTestImagePath();

  const fileChooserPromise = page.waitForEvent("filechooser");
  // Prefer the explicit upload button; on some tool pages the first
  // border-dashed element is a settings section, not the dropzone.
  const uploadButton = page.getByRole("button", { name: /upload from computer/i }).first();
  if (await uploadButton.isVisible({ timeout: 2000 }).catch(() => false)) {
    await uploadButton.click();
  } else {
    await page.locator("[class*='border-dashed']").first().click();
  }
  const fileChooser = await fileChooserPromise;
  await fileChooser.setFiles(testImagePath);

  // Wait for React state to update
  await page.waitForTimeout(500);
}

// ---------------------------------------------------------------------------
// waitForProcessing() — wait for processing to complete
// ---------------------------------------------------------------------------
export async function waitForProcessing(page: Page, timeoutMs = 30_000) {
  try {
    const spinner = page.locator("[class*='animate-spin']");
    if (await spinner.isVisible({ timeout: 2000 })) {
      await spinner.waitFor({ state: "hidden", timeout: timeoutMs });
    }
  } catch {
    // No spinner appeared — processing may have been instant
  }
}

// ---------------------------------------------------------------------------
// Custom test fixture — loggedInPage uses the saved storageState
// (all "chromium" project tests already have auth via storageState,
//  but this provides backward compatibility for tests that use it)
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// putSettings() — write system settings through the API using the page's
// bearer token (the app stores it in localStorage, so page.request alone
// sends no auth).
// ---------------------------------------------------------------------------
async function getAuthToken(page: Page): Promise<string | null> {
  return page.evaluate(() => localStorage.getItem("snapotter-token")).catch(() => null);
}

export async function putSettings(
  page: Page,
  data: Record<string, string>,
): Promise<{ ok: boolean; status: number }> {
  const token = await getAuthToken(page);
  const res = await page.request.put("/api/v1/settings", {
    headers: token ? { authorization: `Bearer ${token}` } : {},
    data,
  });
  return { ok: res.ok(), status: res.status() };
}

/**
 * changePasswordViaApi() — revert a password change without driving the UI.
 * The current session token survives a password change (the API only revokes
 * other sessions), so tests that successfully change the admin password MUST
 * call this to restore it before finishing.
 */
export async function changePasswordViaApi(
  page: Page,
  currentPassword: string,
  newPassword: string,
): Promise<{ ok: boolean; status: number }> {
  const token = await getAuthToken(page);
  const res = await page.request.post("/api/auth/change-password", {
    headers: token ? { authorization: `Bearer ${token}` } : {},
    data: { currentPassword, newPassword },
  });
  return { ok: res.ok(), status: res.status() };
}

export const test = base.extend<{ loggedInPage: Page }>({
  loggedInPage: async ({ page }, use) => {
    // storageState is already loaded by the project config, just navigate
    await page.goto("/");
    // Self-heal global server settings a crashed predecessor may have left
    // mutated (e.g. defaultToolView=fullscreen redirects "/" and hides the
    // sidebar, cascading failures through every later test on the shared DB).
    const healed = await putSettings(page, { defaultToolView: "sidebar", defaultLocale: "en" });
    if (!healed.ok) {
      console.warn(`loggedInPage settings heal failed with status ${healed.status}`);
    }
    if (page.url().includes("/fullscreen")) {
      await page.goto("/");
    }
    await use(page);
  },
});

// ---------------------------------------------------------------------------
// isAiSidecarRunning() — check if the Python AI dispatcher is ready
// ---------------------------------------------------------------------------
export async function isAiSidecarRunning(page: Page): Promise<boolean> {
  try {
    const response = await page.request.get("/api/v1/admin/health");
    if (!response.ok()) return false;
    const health = (await response.json()) as {
      ai?: { dispatcher?: { ready?: boolean; running?: boolean } };
    };
    return health.ai?.dispatcher?.ready === true;
  } catch {
    return false;
  }
}

// ---------------------------------------------------------------------------
// openSettings() — reliably open the Settings dialog across all viewports
// ---------------------------------------------------------------------------
export async function openSettings(page: Page): Promise<void> {
  const sidebar = page.locator("aside");
  if (!(await sidebar.isVisible({ timeout: 2000 }).catch(() => false))) {
    // Fullscreen grid layout hides the aside behind a banner "Sidebar" toggle.
    const sidebarToggle = page.getByRole("button", { name: /^sidebar$/i });
    if (await sidebarToggle.isVisible({ timeout: 1000 }).catch(() => false)) {
      await sidebarToggle.click();
    }
  }
  if (await sidebar.isVisible({ timeout: 2000 }).catch(() => false)) {
    await sidebar.getByText("Settings").click();
  } else {
    await page.getByRole("button", { name: /settings/i }).click();
  }
  await page.getByRole("dialog").waitFor({ state: "visible", timeout: 5000 });
}

export { expect };
