import { Page, expect, Locator } from '@playwright/test';

/**
 * Base Page Object
 * All page objects inherit from this to ensure consistent locator strategy
 * and navigation patterns
 */
export class BasePage {
  protected page: Page;
  protected baseUrl: string;
  protected defaultPollInterval = 500; // Poll every 500ms
  protected defaultPollTimeout = 30000; // Poll for up to 30s

  constructor(page: Page, baseUrl: string = process.env.SUT_URL || 'http://localhost') {
    this.page = page;
    this.baseUrl = baseUrl;
  }

  /**
   * Navigate to a specific path
   */
  async goto(path: string): Promise<void> {
    const url = `${this.baseUrl}${path}`;
    await this.page.goto(url, { waitUntil: 'domcontentloaded' });
    // Don't wait for networkidle - gives time for async data loading
    // Instead wait a bit for React to mount and initial renders
    await this.page.waitForTimeout(500);
  }

  /**
   * Get current URL path
   */
  async getUrlPath(): Promise<string> {
    return new URL(this.page.url()).pathname;
  }

  /**
   * Wait for URL to match pattern
   */
  async waitForUrl(urlPattern: string | RegExp, timeout: number = 30000): Promise<void> {
    await this.page.waitForURL(urlPattern, { timeout });
  }

  /**
   * Check if page is loaded by verifying load event
   * Uses 'load' instead of 'networkidle' to avoid timeouts with async data loading
   */
  async isLoaded(): Promise<boolean> {
    try {
      // Wait for 'load' event which fires after initial page and resources are loaded
      // This is faster than 'networkidle' and more reliable with continuously-loading apps
      await this.page.waitForLoadState('load', { timeout: 5000 });
      return true;
    } catch {
      // If load event doesn't fire, assume page is loaded since goto() already waited for domcontentloaded
      return true;
    }
  }

  /**
   * Wait for element with auto-waiting
   */
  async waitForElement(selector: string, timeout: number = 10000): Promise<void> {
    await this.page.locator(selector).first().waitFor({ state: 'visible', timeout });
  }

  /**
   * Click element by role (preferred method)
   */
  async clickByRole(
    role: string,
    options?: { name?: string | RegExp; exact?: boolean },
  ): Promise<void> {
    await this.page.getByRole(role as any, options).first().click();
  }

  /**
   * Click element by test id
   */
  async clickByTestId(testId: string): Promise<void> {
    await this.page.getByTestId(testId).first().click();
  }

  /**
   * Fill text input
   */
  async fillInput(selector: string, value: string): Promise<void> {
    await this.page.locator(selector).first().fill(value);
  }

  /**
   * Get element text content
   */
  async getElementText(selector: string): Promise<string> {
    const text = await this.page.locator(selector).first().textContent();
    return text || '';
  }

  /**
   * Check if element is visible
   */
  async isElementVisible(selector: string, timeout: number = 5000): Promise<boolean> {
    try {
      await this.page.locator(selector).first().isVisible();
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Wait and verify element text
   */
  async expectElementToHaveText(selector: string, text: string | RegExp): Promise<void> {
    await expect(this.page.locator(selector).first()).toContainText(text);
  }

  /**
   * Dismiss alert via keyboard
   */
  async dismissAlert(): Promise<void> {
    try {
      await this.page.keyboard.press('Escape');
    } catch {
      // Alert may not be present
    }
  }

  /**
   * Actively poll for element visibility with retries
   * Useful for elements that appear after async operations
   */
  async pollForElementVisible(
    locator: Locator | string,
    timeout: number = this.defaultPollTimeout,
    pollInterval: number = this.defaultPollInterval,
  ): Promise<void> {
    const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
    const startTime = Date.now();

    while (Date.now() - startTime < timeout) {
      try {
        const isVisible = await element.isVisible().catch(() => false);
        if (isVisible) {
          return;
        }
      } catch {
        // Element doesn't exist yet, continue polling
      }
      await this.page.waitForTimeout(pollInterval);
    }

    throw new Error(
      `Element did not become visible within ${timeout}ms. Polling interval: ${pollInterval}ms`,
    );
  }

  /**
   * Actively poll for element to contain text
   * Useful for dynamic content that updates after async operations
   */
  async pollForElementText(
    locator: Locator | string,
    expectedText: string | RegExp,
    timeout: number = this.defaultPollTimeout,
    pollInterval: number = this.defaultPollInterval,
  ): Promise<void> {
    const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
    const startTime = Date.now();

    while (Date.now() - startTime < timeout) {
      try {
        const text = await element.textContent().catch(() => '');
        if (text) {
          if (typeof expectedText === 'string') {
            if (text.includes(expectedText)) {
              return;
            }
          } else {
            if (expectedText.test(text)) {
              return;
            }
          }
        }
      } catch {
        // Element doesn't exist yet, continue polling
      }
      await this.page.waitForTimeout(pollInterval);
    }

    throw new Error(
      `Element text "${expectedText}" not found within ${timeout}ms. Polling interval: ${pollInterval}ms`,
    );
  }

  /**
   * Actively poll for any condition
   * Generic polling helper for custom wait conditions
   */
  async pollUntil(
    condition: () => Promise<boolean>,
    timeout: number = this.defaultPollTimeout,
    pollInterval: number = this.defaultPollInterval,
  ): Promise<void> {
    const startTime = Date.now();

    while (Date.now() - startTime < timeout) {
      try {
        const result = await condition();
        if (result) {
          return;
        }
      } catch {
        // Condition threw error, continue polling
      }
      await this.page.waitForTimeout(pollInterval);
    }

    throw new Error(`Condition not met within ${timeout}ms. Polling interval: ${pollInterval}ms`);
  }

  /**
   * Wait for elements count to match expected count
   * Useful for waiting for lists to populate
   */
  async pollForElementCount(
    locator: Locator | string,
    expectedCount: number,
    timeout: number = this.defaultPollTimeout,
    pollInterval: number = this.defaultPollInterval,
  ): Promise<void> {
    const element = typeof locator === 'string' ? this.page.locator(locator) : locator;
    const startTime = Date.now();

    while (Date.now() - startTime < timeout) {
      try {
        const count = await element.count();
        if (count === expectedCount) {
          return;
        }
      } catch {
        // Locator error, continue polling
      }
      await this.page.waitForTimeout(pollInterval);
    }

    throw new Error(
      `Expected element count ${expectedCount} not reached within ${timeout}ms`,
    );
  }
}
