/* eslint-disable react-hooks/rules-of-hooks */
/**
 * Extended Playwright test fixture with FrigateApp.
 *
 * Every test imports `test` and `expect` from this file instead of
 * @playwright/test directly. The `frigateApp` fixture provides a
 * fully mocked Frigate frontend ready for interaction.
 *
 * The fixture also installs the error collector (see error-collector.ts).
 * Any console error, page error, or same-origin failed request that is
 * not on the global allowlist or the test's `expectedErrors` list will
 * fail the test in the fixture's teardown.
 *
 * CRITICAL: All route/WS handlers are registered before page.goto()
 * to prevent AuthProvider from redirecting to login.html.
 */

import { test as base, expect, type Page } from "@playwright/test";
import {
  ApiMocker,
  MediaMocker,
  type ApiMockOverrides,
} from "../helpers/api-mocker";
import { WsMocker } from "../helpers/ws-mocker";
import { installErrorCollector, type ErrorCollector } from "./error-collector";
import { GLOBAL_ALLOWLIST } from "./error-allowlist";

export class FrigateApp {
  public api: ApiMocker;
  public media: MediaMocker;
  public ws: WsMocker;
  public page: Page;

  private isDesktop: boolean;

  constructor(page: Page, projectName: string) {
    this.page = page;
    this.api = new ApiMocker(page);
    this.media = new MediaMocker(page);
    this.ws = new WsMocker();
    this.isDesktop = projectName === "desktop";
  }

  get isMobile() {
    return !this.isDesktop;
  }

  /** Install all mocks with default data. Call before goto(). */
  async installDefaults(overrides?: ApiMockOverrides) {
    // Mock i18n locale files to prevent 404s
    await this.page.route("**/locales/**", async (route) => {
      // Let the request through to the built files
      return route.fallback();
    });

    await this.ws.install(this.page);
    await this.media.install();
    await this.api.install(overrides);
  }

  /** Navigate to a page. Always call installDefaults() first. */
  async goto(path: string) {
    await this.page.goto(path);
    // Wait for the app to render past the loading indicator
    await this.page.waitForSelector("#pageRoot", { timeout: 10_000 });
  }

  /** Navigate to a page that may show a loading indicator */
  async gotoAndWait(path: string, selector: string) {
    await this.page.goto(path);
    await this.page.waitForSelector(selector, { timeout: 10_000 });
  }
}

type FrigateFixtures = {
  frigateApp: FrigateApp;
  /**
   * Per-test additional allowlist regex patterns. Tests that intentionally
   * trigger errors (e.g. error-state tests that hit a mocked 500) declare
   * their expected errors here so the collector ignores them.
   *
   * Default is `[]` — most tests should not need this.
   */
  expectedErrors: RegExp[];
  errorCollector: ErrorCollector;
};

export const test = base.extend<FrigateFixtures>({
  expectedErrors: [[], { option: true }],

  errorCollector: async ({ page, expectedErrors }, use, testInfo) => {
    const collector = installErrorCollector(page, [
      ...GLOBAL_ALLOWLIST,
      ...expectedErrors,
    ]);
    await use(collector);
    if (process.env.E2E_STRICT_ERRORS === "1") {
      collector.assertClean();
    } else if (collector.errors.length > 0) {
      // Soft mode: attach errors to the test report so they're visible
      // without failing the run.
      await testInfo.attach("collected-errors.txt", {
        body: collector.errors
          .map((e) => `[${e.kind}] ${e.message}${e.url ? ` (${e.url})` : ""}`)
          .join("\n"),
        contentType: "text/plain",
      });
    }
  },

  frigateApp: async ({ page, errorCollector }, use, testInfo) => {
    // Reference the collector so its `use()` runs and teardown fires
    void errorCollector;
    const app = new FrigateApp(page, testInfo.project.name);
    await app.installDefaults();
    await use(app);
  },
});

export { expect };
