/**
 * Bug Detector — captures console errors, network issues, layout problems
 */

import type { Page, Request, Response, ConsoleMessage } from 'playwright';
import { SLOW_ENDPOINT_MS, SKIP_URL_PATTERNS } from './config.js';

export type Severity = 'P0' | 'P1' | 'P2';
export type BugArea = 'FE' | 'BE' | 'INFRA';

export interface Bug {
  id: string;
  severity: Severity;
  category: string;
  title: string;
  url: string;
  device: string;
  browser: string;
  stepsToReproduce: string[];
  expected: string;
  actual: string;
  screenshot?: string;
  consoleExcerpt?: string;
  networkExcerpt?: string;
  suggestedOwner: BugArea;
  timestamp: string;
}

interface NetworkEntry {
  url: string;
  method: string;
  status: number;
  duration: number;
  resourceType: string;
  timestamp: number;
}

const NOISY_REQUEST_PATTERNS = [
  /\/api\/pageviews\/track(?:\?|$)/,
  /\/api\/pageviews\/conversion(?:\?|$)/,
  /\/api\/behavior\/track(?:\?|$)/,
  /\/favicon\./,
  /\/sw\.js(?:\?|$)/,
];

function isNoisyRequest(url: string): boolean {
  return SKIP_URL_PATTERNS.some((p) => p.test(url)) || NOISY_REQUEST_PATTERNS.some((p) => p.test(url));
}

function isAbortLikeFailure(errorText: string | undefined): boolean {
  if (!errorText) return false;
  return errorText.includes('ERR_ABORTED')
    || errorText.includes('NS_BINDING_ABORTED')
    || errorText.includes('net::ERR_ABORTED')
    || errorText.includes('Load request cancelled');
}

export class BugDetector {
  private bugs: Bug[] = [];
  private consoleErrors: Array<{ type: string; text: string; url: string; timestamp: number }> = [];
  private networkEntries: NetworkEntry[] = [];
  private networkErrors: NetworkEntry[] = [];
  private pendingRequests = new Map<string, number>();
  private bugCounter = 0;
  private device: string;
  private browser: string;
  private currentSteps: string[] = [];

  constructor(device: string, browser: string) {
    this.device = device;
    this.browser = browser;
  }

  /** Attach listeners to a page */
  attach(page: Page): void {
    // Console monitoring
    page.on('console', (msg: ConsoleMessage) => {
      const type = msg.type();
      if (type === 'error' || type === 'warning') {
        const text = msg.text();
        if (text.includes('favicon') || text.includes('sw.js')) return;
        if (text.includes('TypeError: Load failed')) return;

        this.consoleErrors.push({
          type,
          text,
          url: page.url(),
          timestamp: Date.now(),
        });

        if (type === 'error') {
          this.addBug({
            severity: 'P0',
            category: 'console_error',
            title: `Console error: ${text.slice(0, 120)}`,
            url: page.url(),
            expected: 'No console errors',
            actual: text,
            consoleExcerpt: text.slice(0, 500),
            suggestedOwner: 'FE',
          });
        }
      }
    });

    // Unhandled errors
    page.on('pageerror', (err) => {
      if (err.message.includes('due to access control checks')) return;
      this.addBug({
        severity: 'P0',
        category: 'unhandled_error',
        title: `Unhandled error: ${err.message.slice(0, 120)}`,
        url: page.url(),
        expected: 'No unhandled exceptions',
        actual: err.message,
        consoleExcerpt: err.stack?.slice(0, 500) || err.message,
        suggestedOwner: 'FE',
      });
    });

    // Network monitoring
    page.on('request', (req: Request) => {
      const url = req.url();
      if (isNoisyRequest(url)) return;
      this.pendingRequests.set(url, Date.now());
    });

    page.on('response', (res: Response) => {
      const url = res.url();
      if (isNoisyRequest(url)) return;
      const startTime = this.pendingRequests.get(url);
      if (!startTime) return;
      this.pendingRequests.delete(url);

      const duration = Date.now() - startTime;
      const status = res.status();
      const entry: NetworkEntry = {
        url,
        method: res.request().method(),
        status,
        duration,
        resourceType: res.request().resourceType(),
        timestamp: Date.now(),
      };

      this.networkEntries.push(entry);

      // 5xx = P0
      if (status >= 500) {
        this.networkErrors.push(entry);
        this.addBug({
          severity: 'P0',
          category: 'network_5xx',
          title: `Server error ${status} on ${new URL(url).pathname}`,
          url: page.url(),
          expected: 'Successful response (2xx)',
          actual: `${entry.method} ${url} returned ${status}`,
          networkExcerpt: `${entry.method} ${url} → ${status} (${duration}ms)`,
          suggestedOwner: 'BE',
        });
      }

      // 4xx on app assets (not API auth rejections which are expected logged out)
      if (status >= 400 && status < 500) {
        const isApiAuth = url.includes('/api/') && (status === 401 || status === 403);
        const isAsset = ['stylesheet', 'script', 'image', 'font'].includes(entry.resourceType);
        if (isAsset || (!isApiAuth && status === 404)) {
          this.networkErrors.push(entry);
          this.addBug({
            severity: status === 404 ? 'P0' : 'P1',
            category: 'network_4xx',
            title: `${status} on ${entry.resourceType}: ${new URL(url).pathname}`,
            url: page.url(),
            expected: 'Resource loads successfully',
            actual: `${entry.method} ${url} returned ${status}`,
            networkExcerpt: `${entry.method} ${url} → ${status} (${duration}ms)`,
            suggestedOwner: isAsset ? 'FE' : 'BE',
          });
        }
      }

      // Slow endpoints
      if (duration > SLOW_ENDPOINT_MS && url.includes('/api/')) {
        this.addBug({
          severity: 'P2',
          category: 'slow_endpoint',
          title: `Slow API: ${new URL(url).pathname} took ${duration}ms`,
          url: page.url(),
          expected: `API response < ${SLOW_ENDPOINT_MS}ms`,
          actual: `${entry.method} ${url} took ${duration}ms`,
          networkExcerpt: `${entry.method} ${url} → ${status} (${duration}ms)`,
          suggestedOwner: 'BE',
        });
      }
    });

    page.on('requestfailed', (req: Request) => {
      const url = req.url();
      if (isNoisyRequest(url)) return;
      const failure = req.failure();
      if (isAbortLikeFailure(failure?.errorText)) return;
      this.addBug({
        severity: 'P0',
        category: 'network_failure',
        title: `Request failed: ${new URL(url).pathname}`,
        url: page.url(),
        expected: 'Request completes',
        actual: `${req.method()} ${url} failed: ${failure?.errorText || 'unknown'}`,
        networkExcerpt: `${req.method()} ${url} → FAILED (${failure?.errorText})`,
        suggestedOwner: url.includes('/api/') ? 'BE' : 'FE',
      });
    });
  }

  /** Record a navigation step for repro */
  pushStep(step: string): void {
    this.currentSteps.push(step);
    // Keep last 20 steps for context
    if (this.currentSteps.length > 20) this.currentSteps.shift();
  }

  /** Check for layout issues on current page */
  async checkLayout(page: Page): Promise<void> {
    try {
      const issues = await page.evaluate(() => {
        const problems: Array<{ type: string; selector: string; details: string }> = [];
        const vw = window.innerWidth;
        const vh = window.innerHeight;
        const issueCaps = new Map<string, number>();
        const canRecord = (type: string) => {
          const count = issueCaps.get(type) || 0;
          if (count >= 5) return false;
          issueCaps.set(type, count + 1);
          return true;
        };

        // Check for horizontal overflow
        if (document.documentElement.scrollWidth > vw + 5) {
          problems.push({
            type: 'horizontal_overflow',
            selector: 'html',
            details: `Page width ${document.documentElement.scrollWidth}px exceeds viewport ${vw}px`,
          });
        }

        // Check clickable elements that are off-screen or too small
        const clickables = document.querySelectorAll('a, button, [role="button"], [onclick]');
        clickables.forEach((el) => {
          const rect = el.getBoundingClientRect();
          if (rect.width === 0 && rect.height === 0) return; // hidden, skip
          const style = window.getComputedStyle(el);
          if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return;

          const visibleInViewport = rect.bottom >= 0 && rect.top <= vh && rect.right >= 0 && rect.left <= vw;
          const isControl = el.tagName === 'BUTTON' || el.getAttribute('role') === 'button' || el.hasAttribute('onclick');

          // Off-screen horizontally or clipped off the top is suspicious. Below-the-fold elements are normal.
          if (isControl && rect.bottom >= 0 && rect.top <= vh && (rect.right > vw + 4 || rect.left < -4) && canRecord('off_screen_clickable')) {
            const text = (el.textContent || '').trim().slice(0, 40);
            problems.push({
              type: 'off_screen_clickable',
              selector: el.tagName.toLowerCase() + (el.className ? `.${String(el.className).split(' ')[0]}` : ''),
              details: `Clickable "${text}" at (${Math.round(rect.left)},${Math.round(rect.top)}) is clipped/off-screen`,
            });
          }

          // Too small to tap only matters for controls that are actually visible in the viewport.
          if (visibleInViewport && isControl && rect.width > 0 && rect.height > 0 && (rect.width < 24 || rect.height < 24) && canRecord('too_small_tap_target')) {
            const text = (el.textContent || '').trim().slice(0, 40);
            problems.push({
              type: 'too_small_tap_target',
              selector: el.tagName.toLowerCase() + (el.className ? `.${String(el.className).split(' ')[0]}` : ''),
              details: `Tap target "${text}" is ${Math.round(rect.width)}x${Math.round(rect.height)}px (min 24x24)`,
            });
          }
        });

        // Check for visible popovers/tooltips that are clipped
        const popovers = document.querySelectorAll('[data-popover], [role="tooltip"], .popover, .tooltip, .info-bubble');
        popovers.forEach((el) => {
          const rect = el.getBoundingClientRect();
          const style = window.getComputedStyle(el);
          if (style.display === 'none' || style.visibility === 'hidden') return;
          if (rect.width === 0 || rect.height === 0) return;

          if ((rect.left < 0 || rect.right > vw || rect.top < 0) && canRecord('clipped_popover')) {
            problems.push({
              type: 'clipped_popover',
              selector: el.tagName.toLowerCase() + (el.className ? `.${String(el.className).split(' ')[0]}` : ''),
              details: `Popover clipped: rect(${Math.round(rect.left)},${Math.round(rect.top)},${Math.round(rect.right)},${Math.round(rect.bottom)}) viewport ${vw}x${vh}`,
            });
          }
        });

        // Check for React hydration errors in the DOM
        const body = document.body.innerHTML;
        if (body.includes('Hydration failed') || body.includes('hydration mismatch')) {
          problems.push({
            type: 'hydration_error',
            selector: 'body',
            details: 'React hydration error detected in page content',
          });
        }

        return problems;
      });

      for (const issue of issues) {
        const severityMap: Record<string, Severity> = {
          horizontal_overflow: 'P1',
          off_screen_clickable: 'P2',
          too_small_tap_target: 'P2',
          clipped_popover: 'P1',
          hydration_error: 'P0',
        };

        this.addBug({
          severity: severityMap[issue.type] || 'P2',
          category: `layout_${issue.type}`,
          title: `${issue.type}: ${issue.details.slice(0, 100)}`,
          url: page.url(),
          expected: 'Correct layout without overflow or clipping',
          actual: issue.details,
          suggestedOwner: 'FE',
        });
      }
    } catch {
      // Page might have navigated, skip
    }
  }

  /** Check for scroll lock (body overflow hidden when it shouldn't be) */
  async checkScrollLock(page: Page): Promise<void> {
    try {
      const locked = await page.evaluate(() => {
        const style = window.getComputedStyle(document.body);
        const htmlStyle = window.getComputedStyle(document.documentElement);
        // If a modal is open, overflow:hidden is expected
        const hasModal = document.querySelector('.modal-backdrop, [role="dialog"], .fixed.inset-0');
        if (hasModal) return false;
        return style.overflow === 'hidden' || htmlStyle.overflow === 'hidden';
      });

      if (locked) {
        this.addBug({
          severity: 'P1',
          category: 'scroll_lock',
          title: 'Scroll locked without visible modal',
          url: page.url(),
          expected: 'Page should be scrollable when no modal is open',
          actual: 'body/html has overflow:hidden without a visible modal',
          suggestedOwner: 'FE',
        });
      }
    } catch {
      // ignore
    }
  }

  private addBug(partial: Omit<Bug, 'id' | 'device' | 'browser' | 'stepsToReproduce' | 'timestamp'>): void {
    // Deduplicate: don't add if same title+url+category already exists
    const exists = this.bugs.some(
      (b) => b.title === partial.title && b.url === partial.url && b.category === partial.category
    );
    if (exists) return;

    this.bugCounter++;
    this.bugs.push({
      ...partial,
      id: `BUG-${String(this.bugCounter).padStart(3, '0')}`,
      device: this.device,
      browser: this.browser,
      stepsToReproduce: [...this.currentSteps],
      timestamp: new Date().toISOString(),
    });
  }

  /** Get all collected bugs */
  getBugs(): Bug[] {
    return [...this.bugs];
  }

  /** Get network summary */
  getNetworkSummary(): { total: number; errors: number; slow: number; entries: NetworkEntry[] } {
    return {
      total: this.networkEntries.length,
      errors: this.networkErrors.length,
      slow: this.networkEntries.filter((e) => e.duration > SLOW_ENDPOINT_MS).length,
      entries: this.networkEntries,
    };
  }

  /** Get console error summary */
  getConsoleErrors(): typeof this.consoleErrors {
    return [...this.consoleErrors];
  }
}
