// Terminal dashboard — ANSI escape code UI for E2E test status
// No external deps. Renders test list with status icons, timing, and summary.

export type TestStatus = 'pending' | 'running' | 'passed' | 'failed' | 'skipped';

export interface TestEntry {
  name: string;
  specFile: string;
  status: TestStatus;
  durationMs?: number;
  error?: string;
}

// ── ANSI helpers ──

const ESC = '\x1b[';
const CLEAR_SCREEN = `${ESC}2J${ESC}H`;
const HIDE_CURSOR = `${ESC}?25l`;
const SHOW_CURSOR = `${ESC}?25h`;
const BOLD = `${ESC}1m`;
const DIM = `${ESC}2m`;
const RESET = `${ESC}0m`;

const fg = {
  red: `${ESC}31m`,
  green: `${ESC}32m`,
  yellow: `${ESC}33m`,
  blue: `${ESC}34m`,
  magenta: `${ESC}35m`,
  cyan: `${ESC}36m`,
  white: `${ESC}37m`,
  gray: `${ESC}90m`,
};

const STATUS_ICONS: Record<TestStatus, string> = {
  pending: `${fg.white}\u00b7${RESET}`,   // centered dot
  running: `${fg.yellow}\u27f3${RESET}`,   // clockwise arrow
  passed: `${fg.green}\u2713${RESET}`,     // check mark
  failed: `${fg.red}\u2717${RESET}`,       // cross mark
  skipped: `${fg.gray}\u23ed${RESET}`,     // skip icon
};

function formatDuration(ms: number | undefined): string {
  if (ms === undefined) return '';
  if (ms < 1000) return `${fg.gray}${ms}ms${RESET}`;
  return `${fg.gray}${(ms / 1000).toFixed(1)}s${RESET}`;
}

function truncate(str: string, max: number): string {
  return str.length > max ? str.slice(0, max - 1) + '\u2026' : str;
}

// ── Dashboard ──

export class Dashboard {
  private tests: TestEntry[] = [];
  private startTime: number = Date.now();
  private refreshTimer: ReturnType<typeof setInterval> | null = null;
  private running = false;
  private lastRunStatus: 'idle' | 'running' | 'passed' | 'failed' = 'idle';

  setTests(specs: Array<{ name: string; specFile: string }>): void {
    this.tests = specs.map((s) => ({
      name: s.name,
      specFile: s.specFile,
      status: 'pending' as TestStatus,
    }));
    this.startTime = Date.now();
    this.lastRunStatus = 'running';
  }

  updateTest(name: string, status: TestStatus, durationMs?: number, error?: string): void {
    const entry = this.tests.find((t) => t.name === name);
    if (entry) {
      entry.status = status;
      entry.durationMs = durationMs;
      entry.error = error;
    }
  }

  startRefresh(): void {
    if (this.refreshTimer) return;
    this.running = true;
    process.stdout.write(HIDE_CURSOR);
    this.render();
    this.refreshTimer = setInterval(() => this.render(), 500);
  }

  stopRefresh(): void {
    this.running = false;
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer);
      this.refreshTimer = null;
    }
    // Final render with cursor restored
    this.render();
    process.stdout.write(SHOW_CURSOR);
  }

  markComplete(): void {
    const failed = this.tests.filter((t) => t.status === 'failed').length;
    this.lastRunStatus = failed > 0 ? 'failed' : 'passed';
  }

  stop(): void {
    this.stopRefresh();
  }

  getTests(): TestEntry[] {
    return this.tests;
  }

  render(): void {
    const cols = process.stdout.columns || 80;
    const lines: string[] = [];

    // ── Header ──
    const title = 'Agent Orchestrator \u2014 E2E Test Daemon';
    const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
    const statusColor = this.lastRunStatus === 'failed' ? fg.red
      : this.lastRunStatus === 'passed' ? fg.green
      : this.lastRunStatus === 'running' ? fg.yellow
      : fg.gray;
    const statusLabel = this.running ? 'RUNNING' : this.lastRunStatus.toUpperCase();

    lines.push('');
    lines.push(`  ${BOLD}${fg.cyan}${title}${RESET}  ${statusColor}${statusLabel}${RESET}  ${DIM}${elapsed}s${RESET}`);
    lines.push(`  ${fg.gray}${'─'.repeat(Math.min(cols - 4, 76))}${RESET}`);

    // ── Test list ──
    const nameWidth = Math.min(cols - 20, 60);
    for (const t of this.tests) {
      const icon = STATUS_ICONS[t.status];
      const name = truncate(t.name, nameWidth);
      const dur = formatDuration(t.durationMs);
      lines.push(`  ${icon} ${name}  ${dur}`);
      if (t.status === 'failed' && t.error) {
        const errLine = truncate(t.error, cols - 8);
        lines.push(`    ${fg.red}${DIM}${errLine}${RESET}`);
      }
    }

    // ── Summary footer ──
    const passed = this.tests.filter((t) => t.status === 'passed').length;
    const failed = this.tests.filter((t) => t.status === 'failed').length;
    const skipped = this.tests.filter((t) => t.status === 'skipped').length;
    const running = this.tests.filter((t) => t.status === 'running').length;
    const pending = this.tests.filter((t) => t.status === 'pending').length;
    const totalTime = this.tests.reduce((sum, t) => sum + (t.durationMs ?? 0), 0);

    lines.push('');
    lines.push(`  ${fg.gray}${'─'.repeat(Math.min(cols - 4, 76))}${RESET}`);

    const parts: string[] = [];
    if (passed > 0) parts.push(`${fg.green}${passed} passed${RESET}`);
    if (failed > 0) parts.push(`${fg.red}${failed} failed${RESET}`);
    if (skipped > 0) parts.push(`${fg.gray}${skipped} skipped${RESET}`);
    if (running > 0) parts.push(`${fg.yellow}${running} running${RESET}`);
    if (pending > 0) parts.push(`${fg.white}${pending} pending${RESET}`);
    parts.push(`${DIM}${formatDuration(totalTime)}${RESET}`);

    lines.push(`  ${parts.join('  \u2502  ')}`);
    lines.push('');

    process.stdout.write(CLEAR_SCREEN + lines.join('\n'));
  }
}
