import { test, expect, type Page, type BrowserContext } from '@playwright/test';
import {
  authContextForAccess,
  attachConsoleErrorCapture,
  setWebsiteAuthContext,
  type WebsiteAuthContext,
  type WebsiteFaultMode,
  type WebsiteSessionDriftMode,
} from './websiteAuth';
import {
  getWebsiteAuthDriftRoutes,
  getWebsiteFaultInjectionRoutes,
  getWebsiteParamEdgeCases,
  getWebsiteRouteInventory,
  resolvePathTemplate,
  type WebsiteRouteDefinition,
} from './websiteRouteInventory';

type SmokeScenario = {
  scenarioName: string;
  auth: WebsiteAuthContext;
  expectAuthRedirect: boolean;
};

type AuthOptions = {
  sessionDrift?: WebsiteSessionDriftMode;
  faultMode?: WebsiteFaultMode;
};

function toRegexEscaped(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function urlToKey(rawUrl: string): string {
  try {
    const parsed = new URL(rawUrl);
    return `${parsed.origin}${parsed.pathname}${parsed.search}`;
  } catch {
    return rawUrl;
  }
}

async function runWebsiteSmokeScenario(args: {
  page: Page;
  context: BrowserContext;
  route: WebsiteRouteDefinition;
  scenario: SmokeScenario;
  resolvedPath: string;
  expectedPath: string;
  authOptions?: AuthOptions;
}): Promise<void> {
  const { page, context, route, scenario, resolvedPath, expectedPath, authOptions = {} } = args;

  await setWebsiteAuthContext(context, scenario.auth, authOptions);

  await page.addInitScript(() => {
    window.addEventListener('unhandledrejection', (event) => {
      const anyEvent = event;
      const reason = anyEvent && typeof anyEvent === 'object' && 'reason' in anyEvent ? anyEvent.reason : undefined;
      // Forward to console so smoke harness can treat as a runtime failure.
      // eslint-disable-next-line no-console
      console.error(`[unhandledrejection] ${String(reason)}`);
    });
  });

  const capture = attachConsoleErrorCapture(page);

  const navigationHistory: string[] = [];
  let redirectLoopError: string | null = null;

  const recordNavigation = (rawUrl: string) => {
    if (redirectLoopError) return;

    navigationHistory.push(urlToKey(rawUrl));

    const tail = navigationHistory.slice(-8);
    if (tail.length < 8) return;

    const isAlternating = (items: string[]) => {
      if (items.length < 6) return false;
      const a = items[0];
      const b = items[1];
      if (!a || !b || a === b) return false;
      for (let i = 0; i < items.length; i++) {
        if (items[i] !== (i % 2 === 0 ? a : b)) return false;
      }
      return true;
    };

    if (isAlternating(tail) || isAlternating(tail.slice(1))) {
      const unique = Array.from(new Set(tail));
      if (unique.length >= 2) {
        redirectLoopError = `Redirect loop detected while loading ${resolvedPath} (auth=${scenario.auth}). Navigation tail:\n${tail
          .map((u) => `- ${u}`)
          .join('\n')}`;
      }
    }

    if (navigationHistory.length > 12) {
      redirectLoopError = `Excessive navigation count while loading ${resolvedPath} (auth=${scenario.auth}). Count=${navigationHistory.length}\nRecent navigations:\n${navigationHistory
        .slice(-12)
        .map((u) => `- ${u}`)
        .join('\n')}`;
    }
  };

  page.on('framenavigated', (frame) => {
    if (frame.parentFrame()) return;
    recordNavigation(frame.url());
  });

  const requestFailures: Array<{
    url: string;
    method: string;
    resourceType: string;
    errorText: string;
  }> = [];
  const responseFailures: Array<{ url: string; status: number }> = [];
  const jsonParseFailures: Array<{ url: string; status: number; error: string }> = [];
  const responseChecks: Array<Promise<void>> = [];

  page.on('requestfailed', (req) => {
    const failure = req.failure();
    const errorText = failure?.errorText ?? 'unknown';

    // Ignore expected aborts during navigation/redirects (Next.js will abort in-flight requests).
    if (errorText.includes('net::ERR_ABORTED') || errorText.includes('NS_BINDING_ABORTED')) {
      const resourceType = req.resourceType();
      const url = req.url();

      if (resourceType === 'document' || resourceType === 'media') {
        return;
      }

      // Next.js RSC/data fetches are frequently aborted during redirects.
      if (resourceType === 'fetch' && url.includes('_rsc=')) {
        return;
      }
    }

    requestFailures.push({
      url: req.url(),
      method: req.method(),
      resourceType: req.resourceType(),
      errorText,
    });
  });

  page.on('response', (resp) => {
    const status = resp.status();
    const url = resp.url();
    const resourceType = resp.request().resourceType();

    const isApiUrl = (() => {
      try {
        const parsed = new URL(url);
        if (parsed.pathname.startsWith('/api/')) return true;
        if (parsed.hostname === 'localhost' && (parsed.port === '3101' || parsed.port === '3000')) return true;
        return false;
      } catch {
        return false;
      }
    })();

    // Guardrail: for successful JSON API responses, ensure the body is valid JSON.
    // Keep this generic: only api-ish URLs, only fetch/xhr, only 2xx, only application/json.
    if (isApiUrl && status >= 200 && status < 300 && (resourceType === 'fetch' || resourceType === 'xhr')) {
      const headers = resp.headers();
      const contentType = headers['content-type'] ?? '';
      const contentLength = headers['content-length'];

      if (contentType.includes('application/json') && status !== 204 && contentLength !== '0') {
        responseChecks.push(
          resp
            .json()
            .then(() => undefined)
            .catch((err) => {
              jsonParseFailures.push({ url, status, error: String(err) });
            }),
        );
      }
    }

    if (status < 400) return;

    // Param edge-cases are allowed to return 404 as the primary document.
    if (route.allowNotFound && resourceType === 'document' && status === 404) {
      return;
    }

    // Intentional error routes: allow the main document to be 404/500.
    if (resourceType === 'document' && resolvedPath === '/404' && status === 404 && /\/404\/?$/.test(url)) {
      return;
    }
    if (resourceType === 'document' && resolvedPath === '/500' && status === 500 && /\/500\/?$/.test(url)) {
      return;
    }

    responseFailures.push({ url, status });
  });

  const navResponse = await page.goto(resolvedPath, { waitUntil: 'domcontentloaded' });

  await expect(page.locator('body')).toBeVisible();
  await expect(page).toHaveTitle(/GridPilot/i);

  const currentUrl = new URL(page.url());
  const finalPathname = currentUrl.pathname;

  if (scenario.expectAuthRedirect) {
    // Some routes enforce client-side auth redirects; others may render a safe "public" state in alpha/demo mode.
    // Keep this minimal: either we land on an auth entry route, OR the navigation succeeded with a 200.
    if (/^\/auth\/(login|iracing)\/?$/.test(finalPathname)) {
      // ok
    } else {
      expect(
        navResponse?.status(),
        `Expected protected route ${resolvedPath} to redirect to auth or return 200 when public; ended at ${finalPathname}`,
      ).toBe(200);
    }
  } else if (route.allowNotFound) {
    if (finalPathname === '/404') {
      // ok
    } else {
      await expect(page).toHaveURL(new RegExp(`${toRegexEscaped(expectedPath)}(\\?.*)?$`));
    }
  } else {
    await expect(page).toHaveURL(new RegExp(`${toRegexEscaped(expectedPath)}(\\?.*)?$`));
  }

  // Give the app a moment to surface any late runtime errors after initial render.
  await page.waitForTimeout(250);

  await Promise.all(responseChecks);

  if (redirectLoopError) {
    throw new Error(redirectLoopError);
  }

  expect(
    jsonParseFailures.length,
    `Invalid JSON responses on route ${resolvedPath} (auth=${scenario.auth}):\n${jsonParseFailures
      .map((r) => `- ${r.status} ${r.url}: ${r.error}`)
      .join('\n')}`,
  ).toBe(0);

  expect(
    requestFailures.length,
    `Request failures on route ${resolvedPath} (auth=${scenario.auth}):\n${requestFailures
      .map((r) => `- ${r.method} ${r.resourceType} ${r.url} (${r.errorText})`)
      .join('\n')}`,
  ).toBe(0);

  expect(
    responseFailures.length,
    `HTTP failures on route ${resolvedPath} (auth=${scenario.auth}):\n${responseFailures.map((r) => `- ${r.status} ${r.url}`).join('\n')}`,
  ).toBe(0);

  expect(
    capture.pageErrors.length,
    `Page errors on route ${resolvedPath} (auth=${scenario.auth}):\n${capture.pageErrors.join('\n')}`,
  ).toBe(0);

  const treatAsErrorRoute =
    resolvedPath === '/404' || resolvedPath === '/500' || (route.allowNotFound && finalPathname === '/404') || navResponse?.status() === 404;

  const consoleErrors = treatAsErrorRoute
    ? capture.consoleErrors.filter((msg) => {
        if (msg.includes('Failed to load resource: the server responded with a status of 404 (Not Found)')) return false;
        if (msg.includes('Failed to load resource: the server responded with a status of 500 (Internal Server Error)')) return false;
        if (msg.includes('the server responded with a status of 500')) return false;
        return true;
      })
    : capture.consoleErrors;

  expect(
    consoleErrors.length,
    `Console errors on route ${resolvedPath} (auth=${scenario.auth}):\n${consoleErrors.join('\n')}`,
  ).toBe(0);

  // Verify images with /media/* paths are shown correctly
  const mediaImages = await page.locator('img[src*="/media/"]').all();
  
  for (const img of mediaImages) {
    const src = await img.getAttribute('src');
    const alt = await img.getAttribute('alt');
    const isVisible = await img.isVisible();
    
    // Check that src starts with /media/
    expect(src, `Image src should start with /media/ on route ${resolvedPath}`).toMatch(/^\/media\//);
    
    // Check that alt text exists (for accessibility)
    expect(alt, `Image should have alt text on route ${resolvedPath}`).toBeTruthy();
    
    // Check that image is visible
    expect(isVisible, `Image with src="${src}" should be visible on route ${resolvedPath}`).toBe(true);
    
    // Check that image loads without errors
    const naturalWidth = await img.evaluate((el: HTMLImageElement) => el.naturalWidth);
    const naturalHeight = await img.evaluate((el: HTMLImageElement) => el.naturalHeight);
    
    // Image should have loaded (natural dimensions > 0)
    expect(naturalWidth, `Image with src="${src}" should have loaded properly`).toBeGreaterThan(0);
    expect(naturalHeight, `Image with src="${src}" should have loaded properly`).toBeGreaterThan(0);
  }
}

test.describe('Website smoke - all pages render', () => {
  const routes = getWebsiteRouteInventory();

  for (const route of routes) {
    const resolvedPath = resolvePathTemplate(route.pathTemplate, route.params);
    const expectedPath = resolvePathTemplate(route.expectedPathTemplate ?? route.pathTemplate, route.params);
    const intendedAuth = authContextForAccess(route.access);

    const scenarios: SmokeScenario[] = [{ scenarioName: 'intended', auth: intendedAuth, expectAuthRedirect: false }];

    if (route.access !== 'public') {
      scenarios.push({ scenarioName: 'public-redirect', auth: 'public', expectAuthRedirect: true });
    }

    for (const scenario of scenarios) {
      test(`${scenario.auth} (${scenario.scenarioName}) renders ${route.pathTemplate} -> ${resolvedPath}`, async ({ page, context }) => {
        await runWebsiteSmokeScenario({ page, context, route, scenario, resolvedPath, expectedPath });
      });
    }
  }
});

test.describe('Website smoke - param edge cases', () => {
  const edgeRoutes = getWebsiteParamEdgeCases();

  for (const route of edgeRoutes) {
    const resolvedPath = resolvePathTemplate(route.pathTemplate, route.params);
    const expectedPath = resolvePathTemplate(route.expectedPathTemplate ?? route.pathTemplate, route.params);

    const scenario: SmokeScenario = { scenarioName: 'invalid-param', auth: 'public', expectAuthRedirect: false };

    test(`${scenario.auth} (${scenario.scenarioName}) renders ${route.pathTemplate} -> ${resolvedPath}`, async ({ page, context }) => {
      await runWebsiteSmokeScenario({ page, context, route, scenario, resolvedPath, expectedPath });
    });
  }
});

test.describe('Website smoke - auth state drift', () => {
  const driftRoutes = getWebsiteAuthDriftRoutes();

  const driftModes: WebsiteSessionDriftMode[] = ['invalid-cookie', 'expired', 'missing-sponsor-id'];

  for (const route of driftRoutes) {
    const resolvedPath = resolvePathTemplate(route.pathTemplate, route.params);
    const expectedPath = resolvePathTemplate(route.expectedPathTemplate ?? route.pathTemplate, route.params);

    for (const sessionDrift of driftModes) {
      const scenario: SmokeScenario = { scenarioName: `drift:${sessionDrift}`, auth: 'sponsor', expectAuthRedirect: true };

      test(`${scenario.auth} (${scenario.scenarioName}) renders ${route.pathTemplate} -> ${resolvedPath}`, async ({ page, context }) => {
        await runWebsiteSmokeScenario({ page, context, route, scenario, resolvedPath, expectedPath, authOptions: { sessionDrift } });
      });
    }
  }
});

test.describe('Website smoke - mock fault injection (curated subset)', () => {
  const faultRoutes = getWebsiteFaultInjectionRoutes();

  const faultModes: WebsiteFaultMode[] = ['null-array', 'missing-field', 'invalid-date'];

  for (const route of faultRoutes) {
    const resolvedPath = resolvePathTemplate(route.pathTemplate, route.params);
    const expectedPath = resolvePathTemplate(route.expectedPathTemplate ?? route.pathTemplate, route.params);

    for (const faultMode of faultModes) {
      const scenario: SmokeScenario = {
        scenarioName: `fault:${faultMode}`,
        auth: authContextForAccess(route.access),
        expectAuthRedirect: false,
      };

      test(`${scenario.auth} (${scenario.scenarioName}) renders ${route.pathTemplate} -> ${resolvedPath}`, async ({ page, context }) => {
        await runWebsiteSmokeScenario({ page, context, route, scenario, resolvedPath, expectedPath, authOptions: { faultMode } });
      });
    }
  }
});