import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Page, Locator } from "playwright";
import { pw_assert_class, type PwAssertClassOptions } from "./pw_assert_class";

// ---------------------------------------------------------------------------
// Mock helpers
// ---------------------------------------------------------------------------

/** Build a mock Page with configurable waitForFunction behavior. */
function makeMockPage(opts: {
  waitForFunctionResolves?: boolean;
  waitForFunctionError?: Error;
} = {}): Page {
  const waitForFunction = opts.waitForFunctionError
    ? vi.fn().mockRejectedValue(opts.waitForFunctionError)
    : vi.fn().mockResolvedValue(undefined);

  return {
    waitForFunction,
    locator: vi.fn(),
  } as unknown as Page;
}

/** Build a mock Locator whose evaluate() returns the given classList.has() result. */
function makeMockLocator(hasClass: boolean): Locator {
  return {
    evaluate: vi.fn().mockResolvedValue(hasClass),
  } as unknown as Locator;
}

/** Build a mock Locator whose evaluate() throws (simulates detached element). */
function makeMockLocatorThrows(): Locator {
  return {
    evaluate: vi.fn().mockRejectedValue(new Error("Element not attached")),
  } as unknown as Locator;
}

// ---------------------------------------------------------------------------

beforeEach(() => {
  vi.clearAllMocks();
});

// ---------------------------------------------------------------------------

describe("pw_assert_class", () => {
  // -------------------------------------------------------------------------
  it("has class passes", async () => {
    // mustHave=true (default), element has the class → should resolve immediately.
    const page = makeMockPage({ waitForFunctionResolves: true });
    const opts: PwAssertClassOptions = {
      selector: "[data-card-id='abc']",
      className: "border-red",
      mustHave: true,
      timeoutMs: 3000,
    };

    await expect(pw_assert_class(page, opts)).resolves.toBeUndefined();
    expect((page.waitForFunction as ReturnType<typeof vi.fn>)).toHaveBeenCalledOnce();
  });

  // -------------------------------------------------------------------------
  it("lacks class passes", async () => {
    // mustHave=false, waitForFunction resolves → element does NOT have the class.
    const page = makeMockPage({ waitForFunctionResolves: true });
    const opts: PwAssertClassOptions = {
      selector: ".card",
      className: "highlight-pulse",
      mustHave: false,
      timeoutMs: 3000,
    };

    await expect(pw_assert_class(page, opts)).resolves.toBeUndefined();
  });

  // -------------------------------------------------------------------------
  it("timeout error message", async () => {
    // waitForFunction throws a timeout-like error → pw_assert_class re-throws with detail.
    const page = makeMockPage({
      waitForFunctionError: new Error("Timeout 3000ms exceeded"),
    });
    const opts: PwAssertClassOptions = {
      selector: "[data-card-id='abc']",
      className: "border-red",
      mustHave: true,
      timeoutMs: 3000,
    };

    await expect(pw_assert_class(page, opts)).rejects.toThrow(
      "expected [data-card-id='abc'] to have class border-red within 3000ms"
    );
  });

  // -------------------------------------------------------------------------
  it("string selector resolves", async () => {
    // Verify the string path calls page.waitForFunction with the right serializable args.
    const page = makeMockPage({ waitForFunctionResolves: true });
    const opts: PwAssertClassOptions = {
      selector: "div.card",
      className: "active",
      mustHave: true,
      timeoutMs: 5000,
    };

    await pw_assert_class(page, opts);

    const wff = page.waitForFunction as ReturnType<typeof vi.fn>;
    expect(wff).toHaveBeenCalledOnce();
    // Second arg must include sel, cls, must
    const serializedArg = wff.mock.calls[0][1] as { sel: string; cls: string; must: boolean };
    expect(serializedArg).toMatchObject({ sel: "div.card", cls: "active", must: true });
    // Third arg must include timeout
    const pwOpts = wff.mock.calls[0][2] as { timeout: number };
    expect(pwOpts).toMatchObject({ timeout: 5000 });
  });

  // -------------------------------------------------------------------------
  it("locator polled", async () => {
    // Locator path: evaluate returns true for mustHave=true → resolves without timeout.
    const locator = makeMockLocator(true);
    const page = makeMockPage();
    const opts: PwAssertClassOptions = {
      selector: locator,
      className: "highlight-pulse",
      mustHave: true,
      timeoutMs: 1000,
    };

    await expect(pw_assert_class(page, opts)).resolves.toBeUndefined();

    // waitForFunction should NOT be called for Locator path
    expect((page.waitForFunction as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
    // evaluate called with className
    const evalMock = locator.evaluate as ReturnType<typeof vi.fn>;
    expect(evalMock).toHaveBeenCalledWith(expect.any(Function), "highlight-pulse");
  });

  // -------------------------------------------------------------------------
  it("locator not have class passes", async () => {
    // mustHave=false, element lacks class (evaluate returns false) → condition met.
    const locator = makeMockLocator(false);
    const page = makeMockPage();
    const opts: PwAssertClassOptions = {
      selector: locator,
      className: "border-red",
      mustHave: false,
      timeoutMs: 1000,
    };

    await expect(pw_assert_class(page, opts)).resolves.toBeUndefined();
  });

  // -------------------------------------------------------------------------
  it("locator timeout throws descriptive message", async () => {
    // evaluate always returns false for mustHave=true → exhausts timeout.
    const locator = makeMockLocator(false);
    const page = makeMockPage();
    const opts: PwAssertClassOptions = {
      selector: locator,
      className: "border-red",
      mustHave: true,
      timeoutMs: 150, // Short timeout so test stays fast
    };

    await expect(pw_assert_class(page, opts)).rejects.toThrow(
      "expected <Locator> to have class border-red within 150ms"
    );
  });

  // -------------------------------------------------------------------------
  it("locator evaluate throws treated as not met", async () => {
    // If evaluate throws (detached element), condition is treated as not met;
    // if that persists past timeout, final error is thrown.
    const locator = makeMockLocatorThrows();
    const page = makeMockPage();
    const opts: PwAssertClassOptions = {
      selector: locator,
      className: "border-red",
      mustHave: true,
      timeoutMs: 150,
    };

    await expect(pw_assert_class(page, opts)).rejects.toThrow(
      "expected <Locator> to have class border-red within 150ms"
    );
  });
});
