/**
 * console-protocol.ts is a type-only module describing the Console WS wire
 * protocol. These tests act as a wire-format regression guard:
 *   - compile-time: object literals must `satisfies` each message type, so a
 *     field rename (e.g. acting_user_id → actingUserId) breaks this file
 *   - runtime: each message survives a JSON round-trip losslessly (the actual
 *     transport is JSON text frames, see src/bridge/console-ws-api.ts)
 *   - malformed input: shows how consumers must defend against junk frames
 */
import { describe, it, expect } from 'vitest';
import type {
  AttachMessage,
  ReplayBeginMessage,
  ReplayEndMessage,
  ResizeMessage,
  NoticeMessage,
  CloseMessage,
  ServerTextMessage,
  ClientTextMessage,
  AnyTextMessage,
  SessionCloseReason,
  NoticeSeverity,
} from './console-protocol.js';

/** Minimal runtime validator mirroring what a WS consumer must do. */
function parseTextFrame(raw: string): AnyTextMessage | null {
  let obj: unknown;
  try {
    obj = JSON.parse(raw);
  } catch {
    return null;
  }
  if (typeof obj !== 'object' || obj === null) return null;
  const type = (obj as Record<string, unknown>)['type'];
  if (typeof type !== 'string') return null;
  const known = ['attach', 'replay_begin', 'replay_end', 'resize', 'notice', 'close'];
  if (!known.includes(type)) return null;
  return obj as AnyTextMessage;
}

function roundTrip<T extends AnyTextMessage>(msg: T): T {
  return JSON.parse(JSON.stringify(msg)) as T;
}

describe('ssh/console-protocol wire shapes', () => {
  it('attach message round-trips with snake_case wire fields intact', () => {
    const msg = {
      type: 'attach',
      acting_user_id: 'user-42',
      can_write: true,
      connection_id: 'conn-7',
      cols: 120,
      rows: 40,
    } satisfies AttachMessage;
    const back = roundTrip(msg);
    expect(back).toEqual(msg);
    // wire field names are part of the protocol contract
    expect(Object.keys(back).sort()).toEqual([
      'acting_user_id',
      'can_write',
      'cols',
      'connection_id',
      'rows',
      'type',
    ]);
  });

  it('replay_begin / replay_end round-trip', () => {
    const begin = { type: 'replay_begin', bytes: 8192 } satisfies ReplayBeginMessage;
    const end = { type: 'replay_end' } satisfies ReplayEndMessage;
    expect(roundTrip(begin)).toEqual(begin);
    expect(roundTrip(end)).toEqual(end);
  });

  it('resize message (client → server) round-trips', () => {
    const msg = { type: 'resize', cols: 80, rows: 24 } satisfies ResizeMessage;
    expect(roundTrip(msg)).toEqual(msg);
  });

  it('notice message round-trips for every severity', () => {
    const severities = ['info', 'warn', 'error'] satisfies NoticeSeverity[];
    for (const severity of severities) {
      const msg = { type: 'notice', severity, msg: 'something happened' } satisfies NoticeMessage;
      expect(roundTrip(msg)).toEqual(msg);
    }
  });

  it('close message round-trips for every documented close reason', () => {
    const reasons = [
      'idle_timeout',
      'duration_cap',
      'host_disconnect',
      'maintenance',
      'admin_kill',
      'connection_change',
      'session_cap_evict',
      'worker_shutdown',
      'access_revoked',
    ] satisfies SessionCloseReason[];
    expect(new Set(reasons).size).toBe(reasons.length);
    for (const reason of reasons) {
      const msg = { type: 'close', reason } satisfies CloseMessage;
      expect(roundTrip(msg)).toEqual(msg);
    }
  });

  it('discriminated union narrows on type', () => {
    const msgs: ServerTextMessage[] = [
      { type: 'attach', acting_user_id: 'u', can_write: false, connection_id: 'c', cols: 1, rows: 1 },
      { type: 'replay_begin', bytes: 0 },
      { type: 'replay_end' },
      { type: 'notice', severity: 'warn', msg: 'read-only' },
      { type: 'close', reason: 'admin_kill' },
    ];
    const seen: string[] = [];
    for (const m of msgs) {
      switch (m.type) {
        case 'attach':
          seen.push(`attach:${m.connection_id}`);
          break;
        case 'replay_begin':
          seen.push(`replay_begin:${m.bytes}`);
          break;
        case 'replay_end':
          seen.push('replay_end');
          break;
        case 'notice':
          seen.push(`notice:${m.severity}`);
          break;
        case 'close':
          seen.push(`close:${m.reason}`);
          break;
        default: {
          // exhaustiveness guard — adding a new ServerTextMessage variant
          // without handling it here is a compile error
          const _never: never = m;
          void _never;
        }
      }
    }
    expect(seen).toEqual([
      'attach:c',
      'replay_begin:0',
      'replay_end',
      'notice:warn',
      'close:admin_kill',
    ]);

    const client: ClientTextMessage = { type: 'resize', cols: 100, rows: 30 };
    expect(client.type).toBe('resize');
  });

  it('malformed frames are rejected by a defensive parser', () => {
    expect(parseTextFrame('not json at all')).toBeNull();
    expect(parseTextFrame('')).toBeNull();
    expect(parseTextFrame('null')).toBeNull();
    expect(parseTextFrame('42')).toBeNull();
    expect(parseTextFrame('"resize"')).toBeNull();
    expect(parseTextFrame('[]')).toBeNull();
    expect(parseTextFrame('{}')).toBeNull(); // missing type
    expect(parseTextFrame('{"type":123}')).toBeNull(); // non-string type
    expect(parseTextFrame('{"type":"selfdestruct"}')).toBeNull(); // unknown type
    // valid frame passes
    const ok = parseTextFrame('{"type":"resize","cols":80,"rows":24}');
    expect(ok).toEqual({ type: 'resize', cols: 80, rows: 24 });
  });
});
