/**
 * Shared Test Utilities
 *
 * Provides common functionality for UI tests:
 * - Browser setup/teardown
 * - Configuration management
 * - Helper functions (delay, waitFor, screenshot)
 * - Logging utilities
 */

const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
const AuthHelper = require('../auth_helper');
const { getPuppeteerLaunchOptions } = require('../puppeteer_config');

// Centralized configuration - single source of truth
const config = {
    baseUrl: process.env.BASE_URL || process.env.TEST_BASE_URL || 'http://127.0.0.1:5000',
    isCI: process.env.CI === 'true',
    headless: process.env.HEADLESS !== 'false',
    screenshotDir: path.join(__dirname, '..', 'screenshots'),
    resultsDir: path.join(__dirname, '..', 'test-results'),
    timeout: parseInt(process.env.TEST_TIMEOUT, 10) || 30000,
    slowMo: parseInt(process.env.SLOW_MO, 10) || 0,
};

// Default viewport settings
const viewports = {
    desktop: { width: 1280, height: 800 },
    mobile: { width: 375, height: 667, isMobile: true, hasTouch: true },
    tablet: { width: 768, height: 1024, isMobile: true, hasTouch: true },
};

/**
 * Initialize test environment with browser and optional authentication
 *
 * @param {Object} options - Setup options
 * @param {boolean} [options.authenticate=true] - Whether to authenticate the user
 * @param {boolean} [options.headless] - Override headless mode
 * @param {Object} [options.viewport] - Viewport settings (or use 'desktop', 'mobile', 'tablet')
 * @param {string} [options.username] - Custom username for authentication
 * @param {string} [options.password] - Custom password for authentication
 * @returns {Promise<Object>} Context object with browser, page, authHelper, config
 */
async function setupTest(options = {}) {
    const launchOptions = getPuppeteerLaunchOptions({
        headless: options.headless ?? config.headless,
        slowMo: options.slowMo ?? config.slowMo,
    });

    const browser = await puppeteer.launch(launchOptions);
    const page = await browser.newPage();

    // Set viewport
    let viewport = options.viewport;
    if (typeof viewport === 'string' && viewports[viewport]) {
        viewport = viewports[viewport];
    }
    await page.setViewport(viewport || viewports.desktop);

    // Set default timeouts to avoid indefinite hangs in CI
    const timeout = config.isCI ? 60000 : config.timeout;
    page.setDefaultTimeout(timeout);
    page.setDefaultNavigationTimeout(timeout);

    // Create auth helper
    const authHelper = new AuthHelper(page, config.baseUrl);

    // Authenticate if requested (default: true)
    if (options.authenticate !== false) {
        const authTimeoutMs = config.isCI ? 120000 : 30000;
        await withTimeout(
            authHelper.ensureAuthenticated(options.username, options.password),
            authTimeoutMs,
            'authentication'
        );
    }

    // Return context object
    // Use authHelper.getPage() because ensureAuthenticated() may have created
    // a fresh page if the original one hit a detached frame error
    const finalPage = authHelper.getPage();
    finalPage.setDefaultTimeout(timeout);
    finalPage.setDefaultNavigationTimeout(timeout);

    return {
        browser,
        page: finalPage,
        authHelper,
        config,
        viewports,
    };
}

/**
 * Clean up test environment
 *
 * @param {Object} context - Context object from setupTest
 */
async function teardownTest(context) {
    if (context && context.browser) {
        await context.browser.close();
    }
}

/**
 * Take a screenshot with auto-directory creation
 *
 * @param {Object} page - Puppeteer page object
 * @param {string} name - Screenshot name (without extension)
 * @param {Object} [options] - Screenshot options
 * @param {boolean} [options.fullPage=true] - Capture full page
 * @returns {Promise<string>} Path to saved screenshot
 */
async function screenshot(page, name, options = {}) {
    const dir = config.screenshotDir;
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }

    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const filename = `${name}_${timestamp}.png`;
    const filepath = path.join(dir, filename);

    await page.screenshot({
        path: filepath,
        fullPage: options.fullPage !== false,
        ...options,
    });

    return filepath;
}

/**
 * Simple delay function
 *
 * @param {number} ms - Milliseconds to wait
 * @returns {Promise<void>}
 */
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

/**
 * Run a promise with a timeout. If the promise doesn't resolve within
 * the given time, it rejects with a TimeoutError. This prevents a single
 * hung sub-test from blocking the entire test suite until the 300s
 * process-level timeout kills it.
 *
 * @param {Promise} promise - The promise to race against the timeout
 * @param {number} ms - Timeout in milliseconds (default: 30s CI, 15s local)
 * @param {string} [label] - Description for the timeout error message
 * @returns {Promise} The original promise result or a timeout rejection
 */
function withTimeout(promise, ms, label = 'operation') {
    const timeoutMs = ms || (config.isCI ? 30000 : 15000);
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            reject(new Error(`Timeout: ${label} did not complete within ${timeoutMs / 1000}s`));
        }, timeoutMs);
        promise.then(
            (val) => { clearTimeout(timer); resolve(val); },
            (err) => { clearTimeout(timer); reject(err); }
        );
    });
}

/**
 * Navigate to a page, skipping if already there.
 *
 * Many CI tests call page.goto() for the same URL in every test function.
 * Each navigation takes 5-15s in CI, so 15 tests × 10s = 150s+ of pure
 * navigation overhead. This helper skips the navigation when the page URL
 * already matches, cutting test suite time significantly.
 *
 * @param {Object} page - Puppeteer page object
 * @param {string} url - Full URL to navigate to
 * @param {Object} [options] - Navigation options
 * @returns {Promise<void>}
 */
async function navigateTo(page, url, options = {}) {
    const currentUrl = page.url();
    // Check if already on the target page (compare path, ignore query/hash)
    const targetPath = new URL(url).pathname.replace(/\/$/, '');
    try {
        const currentPath = new URL(currentUrl).pathname.replace(/\/$/, '');
        if (currentPath === targetPath) {
            return null;
        }
    } catch {
        // about:blank or invalid URL — need to navigate
    }

    const response = await page.goto(url, {
        waitUntil: 'domcontentloaded',
        timeout: config.isCI ? 60000 : config.timeout,
        ...options,
    });
    return response;
}

/**
 * Wait for an element with better error messages
 *
 * @param {Object} page - Puppeteer page object
 * @param {string} selector - CSS selector
 * @param {Object} [options] - Wait options
 * @param {number} [options.timeout] - Timeout in ms
 * @param {boolean} [options.visible] - Wait for visible element
 * @returns {Promise<boolean>}
 */
async function waitFor(page, selector, options = {}) {
    const timeout = options.timeout || config.timeout;
    try {
        await page.waitForSelector(selector, {
            timeout,
            visible: options.visible,
        });
        return true;
    } catch {
        throw new Error(`Element "${selector}" not found after ${timeout}ms`);
    }
}

/**
 * Wait for element to become visible (using waitForFunction for reliability)
 *
 * @param {Object} page - Puppeteer page object
 * @param {string} selector - CSS selector
 * @param {number} [timeout] - Timeout in ms
 * @returns {Promise<boolean>}
 */
async function waitForVisible(page, selector, timeout = 5000) {
    try {
        await page.waitForFunction(
            (sel) => {
                const el = document.querySelector(sel);
                if (!el) return false;
                const style = window.getComputedStyle(el);
                return style.display !== 'none' && style.visibility !== 'hidden';
            },
            { timeout },
            selector
        );
        return true;
    } catch {
        return false;
    }
}

/**
 * Click an element and wait for navigation
 *
 * @param {Object} page - Puppeteer page object
 * @param {string} selector - CSS selector to click
 * @param {Object} [options] - Navigation options
 * @returns {Promise<void>}
 */
async function clickAndWaitForNavigation(page, selector, options = {}) {
    const waitUntil = options.waitUntil || 'domcontentloaded';
    const timeout = options.timeout || config.timeout;

    await Promise.all([
        page.waitForNavigation({ waitUntil, timeout }),
        page.click(selector),
    ]);
}

/**
 * Get the value of an input element
 *
 * @param {Object} page - Puppeteer page object
 * @param {string} selector - CSS selector
 * @returns {Promise<string>}
 */
async function getInputValue(page, selector) {
    return page.$eval(selector, el => el.value);
}

/**
 * Clear and type into an input
 *
 * @param {Object} page - Puppeteer page object
 * @param {string} selector - CSS selector
 * @param {string} text - Text to type
 * @returns {Promise<void>}
 */
async function clearAndType(page, selector, text) {
    await page.click(selector, { clickCount: 3 });
    await page.keyboard.press('Backspace');
    await page.type(selector, text);
}

/**
 * Find an action button on the current page by word-boundary keyword match
 * against its text content, and optionally click it.
 *
 * Word boundaries (`\b`) keep the match from firing on substrings — e.g.
 * the default keyword `new` matches "New Folder" but NOT "Back to News
 * Feed" (where "news" merely contains "new"). The original substring
 * matcher caused #4069.
 *
 * @param {import('puppeteer').Page} page
 * @param {Object} [options]
 * @param {string}   [options.selectors] CSS selector list to scan.
 *   Default: `'button, a.btn, .btn'`.
 * @param {string[]} [options.keywords] Lowercase words to match (combined
 *   into `\b(?:k1|k2|...)\b`). Default: `['create', 'new', 'add']`.
 * @param {boolean}  [options.click] If true, click the first match.
 * @returns {Promise<{found: boolean, text?: string}>}
 */
async function findActionButton(page, options = {}) {
    const {
        selectors = 'button, a.btn, .btn',
        keywords = ['create', 'new', 'add'],
        click = false,
    } = options;

    return await page.evaluate((selectorsStr, keywordsArr, doClick) => {
        const pattern = new RegExp(`\\b(?:${keywordsArr.join('|')})\\b`);
        const buttons = Array.from(document.querySelectorAll(selectorsStr));
        const match = buttons.find(b => pattern.test((b.textContent || '').toLowerCase()));
        if (!match) return { found: false };
        if (doClick) match.click();
        return { found: true, text: match.textContent?.trim() };
    }, selectors, keywords, click);
}

// Console logging with colors (works in terminal)
const log = {
    info: (msg) => console.log(`\x1b[36m[INFO] ${msg}\x1b[0m`),
    success: (msg) => console.log(`\x1b[32m[PASS] ${msg}\x1b[0m`),
    error: (msg) => console.log(`\x1b[31m[FAIL] ${msg}\x1b[0m`),
    warning: (msg) => console.log(`\x1b[33m[WARN] ${msg}\x1b[0m`),
    section: (msg) => console.log(`\x1b[34m\n=== ${msg} ===\x1b[0m`),
    debug: (msg) => {
        if (process.env.DEBUG) {
            console.log(`\x1b[90m[DEBUG] ${msg}\x1b[0m`);
        }
    },
};

module.exports = {
    config,
    viewports,
    setupTest,
    teardownTest,
    screenshot,
    delay,
    withTimeout,
    navigateTo,
    waitFor,
    waitForVisible,
    clickAndWaitForNavigation,
    getInputValue,
    clearAndType,
    findActionButton,
    log,
};
