import { describe, it, expect } from 'vitest';
import { wrapOutput, buildPayload } from './output.js';

describe('ssh/output wrapOutput', () => {
  it('returns valid JSON with the standard fields', () => {
    const out = wrapOutput({
      stdout: Buffer.from('hello\n'),
      stderr: Buffer.from(''),
      exitCode: 0,
      durationMs: 42,
    });
    const parsed = JSON.parse(out);
    expect(parsed.stdout).toBe('hello\n');
    expect(parsed.stderr).toBe('');
    expect(parsed.exit_code).toBe(0);
    expect(parsed.duration_ms).toBe(42);
    expect(parsed.truncated).toBe(false);
    expect(parsed.untrusted).toBe(true);
    expect(parsed.notice).toMatch(/untrusted remote host/);
    expect(parsed.stdout_bytes_raw).toBe(6);
    expect(parsed.stderr_bytes_raw).toBe(0);
  });

  it('escapes injection attempts via JSON.stringify', () => {
    const evil = '"}],"injected":true,"x":[{"';
    const out = wrapOutput({
      stdout: Buffer.from(evil),
      stderr: Buffer.from(''),
      exitCode: 0,
      durationMs: 1,
    });
    // The injected text is escaped, so JSON.parse roundtrips cleanly back to the
    // original string — not as a sibling field.
    const parsed = JSON.parse(out);
    expect(parsed.stdout).toBe(evil);
    expect(parsed.injected).toBeUndefined();
  });

  it('preserves control characters via JSON escapes (no terminal injection)', () => {
    const dangerous = '\x1b[31mRED\x1b[0m\nbel: \x07\n';
    const out = wrapOutput({
      stdout: Buffer.from(dangerous),
      stderr: Buffer.from(''),
      exitCode: 0,
      durationMs: 1,
    });
    // The raw bytes \x1b and \x07 must be escaped — they should not appear
    // literally in the JSON output stream.
    expect(out.includes('\x1b')).toBe(false);
    expect(out.includes('\x07')).toBe(false);
    // But after JSON.parse, the application-level string is intact.
    const parsed = JSON.parse(out);
    expect(parsed.stdout).toBe(dangerous);
  });

  it('redacts password=secret-like patterns in stdout', () => {
    const out = wrapOutput({
      stdout: Buffer.from('password=hunter2 next-token=abc123def'),
      stderr: Buffer.from('Bearer abcdef.0987654321'),
      exitCode: 0,
      durationMs: 1,
    });
    const parsed = JSON.parse(out);
    expect(parsed.stdout).not.toMatch(/hunter2/);
    expect(parsed.stdout).toMatch(/password=\[redacted\]/);
    expect(parsed.stderr).toMatch(/Bearer \[redacted\]/);
  });

  it('caps stdout at capBytes and marks truncated=true', () => {
    const big = Buffer.from('a'.repeat(1024));
    const out = wrapOutput({
      stdout: big,
      stderr: Buffer.from(''),
      exitCode: 0,
      durationMs: 1,
      capBytes: 100,
    });
    const parsed = JSON.parse(out);
    expect(parsed.truncated).toBe(true);
    expect(parsed.stdout.length).toBeLessThanOrEqual(120); // capBytes + marker
    expect(parsed.stdout_bytes_raw).toBe(1024);
    expect(parsed.stdout).toMatch(/\[truncated;.*bytes/);
  });

  it('caps stderr independently from stdout', () => {
    const out = wrapOutput({
      stdout: Buffer.from('ok'),
      stderr: Buffer.from('x'.repeat(2000)),
      exitCode: 0,
      durationMs: 1,
      capBytes: 100,
    });
    const parsed = JSON.parse(out);
    expect(parsed.truncated).toBe(true);
    expect(parsed.stderr).toMatch(/\[truncated;/);
    expect(parsed.stdout).toBe('ok'); // not truncated
  });

  it('preserves UTF-8 codepoint boundaries when truncating', () => {
    // 4-byte emoji repeated; if we slice mid-codepoint we'd see replacement chars.
    const emoji = '🤖'.repeat(200); // 200 codepoints, 800 bytes
    const payload = buildPayload({
      stdout: Buffer.from(emoji, 'utf-8'),
      stderr: Buffer.from(''),
      exitCode: 0,
      durationMs: 1,
      capBytes: 100,
    });
    expect(payload.truncated).toBe(true);
    // No replacement character — slice landed on a codepoint boundary.
    expect(payload.stdout.includes('�')).toBe(false);
  });

  it('handles cap=0 by replacing stream with just the marker', () => {
    const out = wrapOutput({
      stdout: Buffer.from('any content'),
      stderr: Buffer.from(''),
      exitCode: 0,
      durationMs: 1,
      capBytes: 0,
    });
    const parsed = JSON.parse(out);
    expect(parsed.truncated).toBe(true);
    expect(parsed.stdout).toMatch(/\[truncated;/);
  });

  it('throws on negative cap', () => {
    expect(() =>
      buildPayload({
        stdout: Buffer.from(''),
        stderr: Buffer.from(''),
        exitCode: 0,
        durationMs: 1,
        capBytes: -1,
      }),
    ).toThrow(/capBytes/);
  });

  it('preserves non-zero exit code', () => {
    const out = wrapOutput({
      stdout: Buffer.from(''),
      stderr: Buffer.from('bad'),
      exitCode: 127,
      durationMs: 5,
    });
    const parsed = JSON.parse(out);
    expect(parsed.exit_code).toBe(127);
    expect(parsed.stderr).toBe('bad');
  });

  it('produces stable, pretty-printed output (2-space indent)', () => {
    const out = wrapOutput({
      stdout: Buffer.from('hi'),
      stderr: Buffer.from(''),
      exitCode: 0,
      durationMs: 1,
    });
    expect(out).toMatch(/^\{\n  "stdout":/);
  });
});
