/**
 * P1 Unit Tests: Eval Server Formatters
 *
 * Tests: formatQueryResult, formatContextResult, formatImpactResult,
 * formatCypherResult, formatDetectChangesResult, formatListReposResult, MAX_BODY_SIZE
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
  formatQueryResult,
  formatContextResult,
  formatImpactResult,
  formatCypherResult,
  formatDetectChangesResult,
  formatListReposResult,
  MAX_BODY_SIZE,
  validateHost,
} from '../../src/cli/eval-server.js';

// ─── validateHost ────────────────────────────────────────────────────

beforeEach(() => {
  vi.unstubAllEnvs();
  vi.stubEnv('GITNEXUS_LANG', 'en');
});

afterEach(() => {
  vi.unstubAllEnvs();
});

describe('validateHost', () => {
  it('passes "localhost" through unchanged', () => {
    expect(validateHost('localhost')).toBe('localhost');
  });

  it('accepts valid IPv4 addresses', () => {
    expect(validateHost('127.0.0.1')).toBe('127.0.0.1');
    expect(validateHost('0.0.0.0')).toBe('0.0.0.0');
    expect(validateHost('192.168.1.5')).toBe('192.168.1.5');
    expect(validateHost('10.0.0.1')).toBe('10.0.0.1');
  });

  it('accepts valid IPv6 addresses', () => {
    expect(validateHost('::1')).toBe('::1');
    expect(validateHost('::')).toBe('::');
    expect(validateHost('2001:db8::1')).toBe('2001:db8::1');
  });

  it('returns null for a non-IP hostname', () => {
    expect(validateHost('foo.bar')).toBeNull();
    expect(validateHost('myhost.local')).toBeNull();
    expect(validateHost('example.com')).toBeNull();
  });

  it('returns null for out-of-range IPv4 octets', () => {
    expect(validateHost('999.999.999.999')).toBeNull();
    expect(validateHost('192.168.1.256')).toBeNull();
  });

  it('returns null for incomplete IPv4 addresses', () => {
    expect(validateHost('192.168.1')).toBeNull();
    expect(validateHost('192.168')).toBeNull();
  });

  it('returns null for an empty string', () => {
    expect(validateHost('')).toBeNull();
  });

  it('returns null for whitespace or padded IPs', () => {
    expect(validateHost(' ')).toBeNull();
    expect(validateHost(' 127.0.0.1')).toBeNull();
    expect(validateHost('127.0.0.1 ')).toBeNull();
  });
});

// ─── MAX_BODY_SIZE ───────────────────────────────────────────────────

describe('MAX_BODY_SIZE', () => {
  it('is 1MB', () => {
    expect(MAX_BODY_SIZE).toBe(1024 * 1024);
  });
});

// ─── formatQueryResult ───────────────────────────────────────────────

describe('formatQueryResult', () => {
  it('returns error message for error input', () => {
    expect(formatQueryResult({ error: 'something failed' })).toBe('Error: something failed');
  });

  it('returns no-match message for empty results', () => {
    const result = formatQueryResult({ processes: [], definitions: [] });
    expect(result).toContain('No matching execution flows');
  });

  it('formats processes with symbols', () => {
    const result = formatQueryResult({
      processes: [{ id: 'p1', summary: 'User Login Flow', step_count: 3, symbol_count: 2 }],
      process_symbols: [
        {
          process_id: 'p1',
          type: 'Function',
          name: 'login',
          filePath: 'src/auth.ts',
          startLine: 10,
        },
        {
          process_id: 'p1',
          type: 'Function',
          name: 'validate',
          filePath: 'src/auth.ts',
          startLine: 20,
        },
      ],
      definitions: [],
    });
    expect(result).toContain('1 execution flow');
    expect(result).toContain('User Login Flow');
    expect(result).toContain('login');
    expect(result).toContain(':10');
  });

  it('truncates symbols per process at 6', () => {
    const symbols = Array.from({ length: 10 }, (_, i) => ({
      process_id: 'p1',
      type: 'Function',
      name: `fn${i}`,
      filePath: 'src/test.ts',
    }));
    const result = formatQueryResult({
      processes: [{ id: 'p1', summary: 'Flow', step_count: 10, symbol_count: 10 }],
      process_symbols: symbols,
      definitions: [],
    });
    expect(result).toContain('and 4 more');
  });

  it('formats standalone definitions', () => {
    const result = formatQueryResult({
      processes: [],
      definitions: [{ type: 'Interface', name: 'Config', filePath: 'src/types.ts' }],
    });
    expect(result).toContain('Standalone definitions');
    expect(result).toContain('Config');
  });

  it('truncates definitions at 8', () => {
    const defs = Array.from({ length: 12 }, (_, i) => ({
      type: 'Interface',
      name: `Type${i}`,
      filePath: 'src/types.ts',
    }));
    const result = formatQueryResult({ processes: [], definitions: defs });
    expect(result).toContain('and 4 more');
  });
});

// ─── formatContextResult ─────────────────────────────────────────────

describe('formatContextResult', () => {
  it('returns error message for error input', () => {
    expect(formatContextResult({ error: 'not found' })).toBe('Error: not found');
  });

  it('handles ambiguous results', () => {
    const result = formatContextResult({
      status: 'ambiguous',
      candidates: [
        { name: 'foo', kind: 'Function', filePath: 'src/a.ts', line: 10, uid: 'uid1' },
        { name: 'foo', kind: 'Function', filePath: 'src/b.ts', line: 5, uid: 'uid2' },
      ],
    });
    expect(result).toContain('Multiple symbols');
    expect(result).toContain('uid1');
    expect(result).toContain('uid2');
  });

  it('returns "Symbol not found" when no symbol', () => {
    expect(formatContextResult({})).toBe('Symbol not found.');
  });

  it('formats symbol with incoming/outgoing refs', () => {
    const result = formatContextResult({
      symbol: { kind: 'Function', name: 'foo', filePath: 'src/a.ts', startLine: 1, endLine: 10 },
      incoming: {
        CALLS: [{ kind: 'Function', name: 'bar', filePath: 'src/b.ts' }],
      },
      outgoing: {
        IMPORTS: [{ kind: 'Module', name: 'utils', filePath: 'src/utils.ts' }],
      },
      processes: [],
    });
    expect(result).toContain('Function foo');
    expect(result).toContain('Called/imported by (1)');
    expect(result).toContain('Calls/imports (1)');
  });

  it('formats process participation', () => {
    const result = formatContextResult({
      symbol: { kind: 'Function', name: 'foo', filePath: 'src/a.ts' },
      incoming: {},
      outgoing: {},
      processes: [{ name: 'Auth Flow', step_index: 2, step_count: 5 }],
    });
    expect(result).toContain('1 execution flow');
    expect(result).toContain('Auth Flow');
  });
});

// ─── formatImpactResult ──────────────────────────────────────────────

describe('formatImpactResult', () => {
  it('returns error message for error input', () => {
    expect(formatImpactResult({ error: 'bad request' })).toContain('Error: bad request');
  });

  it('returns error with suggestion when provided', () => {
    const result = formatImpactResult({
      error: 'Impact analysis failed',
      suggestion: 'Try gitnexus context <symbol> as a fallback',
    });
    expect(result).toContain('Error: Impact analysis failed');
    expect(result).toContain('Suggestion: Try gitnexus context');
  });

  it('shows partial warning when traversal was interrupted', () => {
    const result = formatImpactResult({
      target: { kind: 'Function', name: 'foo' },
      direction: 'upstream',
      impactedCount: 2,
      partial: true,
      byDepth: {
        1: [
          {
            type: 'Function',
            name: 'caller1',
            filePath: 'src/a.ts',
            relationType: 'CALLS',
            confidence: 1,
          },
          {
            type: 'Function',
            name: 'caller2',
            filePath: 'src/b.ts',
            relationType: 'CALLS',
            confidence: 1,
          },
        ],
      },
    });
    expect(result).toContain('Partial results');
    expect(result).toContain('caller1');
    expect(result).toContain('caller2');
  });

  it('handles zero impact', () => {
    const result = formatImpactResult({
      target: { name: 'foo' },
      direction: 'upstream',
      impactedCount: 0,
      byDepth: {},
    });
    expect(result).toContain('No upstream dependencies');
  });

  it('formats impact by depth', () => {
    const result = formatImpactResult({
      target: { kind: 'Function', name: 'foo' },
      direction: 'upstream',
      impactedCount: 3,
      byDepth: {
        1: [
          {
            type: 'Function',
            name: 'caller1',
            filePath: 'src/a.ts',
            relationType: 'CALLS',
            confidence: 1,
          },
          {
            type: 'Function',
            name: 'caller2',
            filePath: 'src/b.ts',
            relationType: 'CALLS',
            confidence: 0.8,
          },
        ],
        2: [
          {
            type: 'Class',
            name: 'App',
            filePath: 'src/app.ts',
            relationType: 'IMPORTS',
            confidence: 1,
          },
        ],
      },
    });
    expect(result).toContain('Blast radius');
    expect(result).toContain('WILL BREAK');
    expect(result).toContain('caller1');
    expect(result).toContain('conf: 0.8');
    expect(result).toContain('LIKELY AFFECTED');
  });

  it('truncates items per depth at 12', () => {
    const items = Array.from({ length: 15 }, (_, i) => ({
      type: 'Function',
      name: `fn${i}`,
      filePath: 'src/test.ts',
      relationType: 'CALLS',
      confidence: 1,
    }));
    const result = formatImpactResult({
      target: { kind: 'Function', name: 'foo' },
      direction: 'upstream',
      impactedCount: 15,
      byDepth: { 1: items },
    });
    expect(result).toContain('and 3 more');
  });
});

// ─── formatCypherResult ──────────────────────────────────────────────

describe('formatCypherResult', () => {
  it('returns error message for error input', () => {
    expect(formatCypherResult({ error: 'syntax error' })).toBe('Error: syntax error');
  });

  it('handles empty array', () => {
    expect(formatCypherResult([])).toBe('Query returned 0 rows.');
  });

  it('formats array of objects as table', () => {
    const result = formatCypherResult([
      { name: 'foo', filePath: 'src/a.ts' },
      { name: 'bar', filePath: 'src/b.ts' },
    ]);
    expect(result).toContain('2 row(s)');
    expect(result).toContain('name: foo');
    expect(result).toContain('name: bar');
  });

  it('truncates at 30 rows', () => {
    const rows = Array.from({ length: 35 }, (_, i) => ({ id: i }));
    const result = formatCypherResult(rows);
    expect(result).toContain('5 more rows');
  });

  it('handles string result', () => {
    expect(formatCypherResult('some text')).toBe('some text');
  });
});

// ─── formatDetectChangesResult ───────────────────────────────────────

describe('formatDetectChangesResult', () => {
  it('returns error message for error input', () => {
    expect(formatDetectChangesResult({ error: 'git error' })).toBe('Error: git error');
  });

  it('handles no changes', () => {
    const result = formatDetectChangesResult({ summary: { changed_count: 0 } });
    expect(result).toBe('No changes detected.');
  });

  it('formats changes with affected processes', () => {
    const result = formatDetectChangesResult({
      summary: { changed_files: 2, changed_count: 3, affected_count: 1, risk_level: 'MEDIUM' },
      changed_symbols: [{ type: 'Function', name: 'foo', filePath: 'src/a.ts' }],
      affected_processes: [
        { name: 'Auth Flow', step_count: 5, changed_steps: [{ symbol: 'foo' }] },
      ],
    });
    expect(result).toContain('2 files');
    expect(result).toContain('MEDIUM');
    expect(result).toContain('Auth Flow');
  });

  it('truncates changed symbols at 15', () => {
    const symbols = Array.from({ length: 20 }, (_, i) => ({
      type: 'Function',
      name: `fn${i}`,
      filePath: 'src/test.ts',
    }));
    const result = formatDetectChangesResult({
      summary: { changed_files: 1, changed_count: 20, affected_count: 0, risk_level: 'HIGH' },
      changed_symbols: symbols,
      affected_processes: [],
    });
    expect(result).toContain('and 5 more');
  });

  it('localizes detect_changes labels for Simplified Chinese', () => {
    vi.stubEnv('GITNEXUS_LANG', 'zh-CN');

    const result = formatDetectChangesResult({
      summary: { changed_files: 2, changed_count: 3, affected_count: 1, risk_level: 'MEDIUM' },
      changed_symbols: [{ type: 'Function', name: 'foo', filePath: 'src/a.ts' }],
      affected_processes: [
        { name: 'Auth Flow', step_count: 5, changed_steps: [{ symbol: 'foo' }] },
      ],
    });

    expect(result).toContain('变更：2 个文件，3 个符号');
    expect(result).toContain('受影响流程：1');
    expect(result).toContain('风险等级：MEDIUM');
    expect(result).toContain('已变更符号：');
    expect(result).toContain('受影响执行流程：');
    expect(result).toContain('Auth Flow (5 步) — 已变更：foo');
  });
});

// ─── formatListReposResult ───────────────────────────────────────────

describe('formatListReposResult', () => {
  it('handles empty/null input', () => {
    expect(formatListReposResult([])).toBe('No indexed repositories.');
    expect(formatListReposResult(null)).toBe('No indexed repositories.');
  });

  it('formats repo list', () => {
    const result = formatListReposResult([
      {
        name: 'my-project',
        path: '/home/user/my-project',
        indexedAt: '2024-01-01',
        stats: { nodes: 100, edges: 200, processes: 10 },
      },
    ]);
    expect(result).toContain('Indexed repositories');
    expect(result).toContain('my-project');
    expect(result).toContain('100 symbols');
  });
});
