// @vitest-environment node
/**
 * task-finalize.ts のユニットテスト。
 * TDD: このファイルを先に書いて失敗させてから実装する。
 */
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import { describe, it, expect } from 'vitest';
import {
  enumerateOutputArtifacts,
  finalizeStatusFromJob,
  ARTIFACT_INLINE_LIMIT,
} from './task-finalize.js';

// ── enumerateOutputArtifacts ─────────────────────────────────────────────────

describe('enumerateOutputArtifacts', () => {
  it('小さいテキストファイルは utf-8 インラインになる', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tf-test-'));
    try {
      const outputDir = path.join(tmpDir, 'output');
      fs.mkdirSync(outputDir);
      fs.writeFileSync(path.join(outputDir, 'hello.txt'), 'Hello world');

      const arts = enumerateOutputArtifacts(tmpDir);
      expect(arts).toHaveLength(1);
      expect(arts[0].name).toBe('hello.txt');
      expect(arts[0].text).toBe('Hello world');
      expect(arts[0].artifactId).toBeTruthy();
      // artifactId は UUID 形式
      expect(arts[0].artifactId).toMatch(
        /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
      );
    } finally {
      fs.rmSync(tmpDir, { recursive: true });
    }
  });

  it('1 MiB 超過ファイルは omitted メッセージになる', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tf-test-'));
    try {
      const outputDir = path.join(tmpDir, 'output');
      fs.mkdirSync(outputDir);
      // ARTIFACT_INLINE_LIMIT + 1 バイトのファイル
      const bigBuf = Buffer.alloc(ARTIFACT_INLINE_LIMIT + 1, 0x41); // 'A' × (1MiB+1)
      fs.writeFileSync(path.join(outputDir, 'big.bin'), bigBuf);

      const arts = enumerateOutputArtifacts(tmpDir);
      expect(arts).toHaveLength(1);
      expect(arts[0].name).toBe('big.bin');
      expect(arts[0].text).toMatch(/\[file big\.bin omitted: \d+ bytes exceeds inline limit\]/);
      expect(arts[0].text).toContain(`${ARTIFACT_INLINE_LIMIT + 1} bytes`);
    } finally {
      fs.rmSync(tmpDir, { recursive: true });
    }
  });

  it('先頭 8KB に null byte があるファイルは not inlined メッセージになる', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tf-test-'));
    try {
      const outputDir = path.join(tmpDir, 'output');
      fs.mkdirSync(outputDir);
      // null byte を先頭付近に含む 100 バイトのファイル
      const binBuf = Buffer.alloc(100, 0x41);
      binBuf[5] = 0x00;
      fs.writeFileSync(path.join(outputDir, 'data.bin'), binBuf);

      const arts = enumerateOutputArtifacts(tmpDir);
      expect(arts).toHaveLength(1);
      expect(arts[0].name).toBe('data.bin');
      expect(arts[0].text).toMatch(/\[binary file data\.bin \(\d+ bytes\) not inlined\]/);
      expect(arts[0].text).toContain('100 bytes');
    } finally {
      fs.rmSync(tmpDir, { recursive: true });
    }
  });

  it('workspacePath が null のとき空配列を返す', () => {
    const result = enumerateOutputArtifacts(null);
    expect(result).toEqual([]);
  });

  it('output ディレクトリが存在しないとき空配列を返す', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tf-test-'));
    try {
      // output/ を作らない
      const result = enumerateOutputArtifacts(tmpDir);
      expect(result).toEqual([]);
    } finally {
      fs.rmSync(tmpDir, { recursive: true });
    }
  });

  it('ディレクトリエントリはスキップする', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tf-test-'));
    try {
      const outputDir = path.join(tmpDir, 'output');
      fs.mkdirSync(outputDir);
      fs.mkdirSync(path.join(outputDir, 'subdir'));
      fs.writeFileSync(path.join(outputDir, 'file.txt'), 'content');

      const arts = enumerateOutputArtifacts(tmpDir);
      expect(arts).toHaveLength(1);
      expect(arts[0].name).toBe('file.txt');
    } finally {
      fs.rmSync(tmpDir, { recursive: true });
    }
  });

  it('各 artifact は異なる artifactId を持つ', () => {
    const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tf-test-'));
    try {
      const outputDir = path.join(tmpDir, 'output');
      fs.mkdirSync(outputDir);
      fs.writeFileSync(path.join(outputDir, 'a.txt'), 'A');
      fs.writeFileSync(path.join(outputDir, 'b.txt'), 'B');

      const arts = enumerateOutputArtifacts(tmpDir);
      expect(arts).toHaveLength(2);
      expect(arts[0].artifactId).not.toBe(arts[1].artifactId);
    } finally {
      fs.rmSync(tmpDir, { recursive: true });
    }
  });
});

// ── finalizeStatusFromJob ────────────────────────────────────────────────────

describe('finalizeStatusFromJob', () => {
  it('succeeded → state: completed (message なし)', () => {
    const result = finalizeStatusFromJob({ status: 'succeeded' });
    expect(result).toEqual({ state: 'completed' });
    expect(result.message).toBeUndefined();
  });

  it('cancelled → state: canceled (message なし)', () => {
    const result = finalizeStatusFromJob({ status: 'cancelled' });
    expect(result).toEqual({ state: 'canceled' });
    expect(result.message).toBeUndefined();
  });

  it('waiting_human → state: input-required + fixed message', () => {
    const result = finalizeStatusFromJob({ status: 'waiting_human' });
    expect(result.state).toBe('input-required');
    expect(result.message).toBe('interactive input not supported in this version');
  });

  it('failed → state: failed + errorSummary をそのまま message に', () => {
    const result = finalizeStatusFromJob({
      status: 'failed',
      errorSummary: 'something broke',
      abortReason: null,
    });
    expect(result.state).toBe('failed');
    expect(result.message).toBe('something broke');
  });

  it('failed + errorSummary null → abortReason を message に', () => {
    const result = finalizeStatusFromJob({
      status: 'failed',
      errorSummary: null,
      abortReason: 'agent aborted',
    });
    expect(result.state).toBe('failed');
    expect(result.message).toBe('agent aborted');
  });

  it('failed + 両方 null → フォールバック文字列', () => {
    const result = finalizeStatusFromJob({
      status: 'failed',
      errorSummary: null,
      abortReason: null,
    });
    expect(result.state).toBe('failed');
    expect(result.message).toBeTruthy();
  });

  it('未知のステータス → state: failed', () => {
    const result = finalizeStatusFromJob({ status: 'unknown_status' });
    expect(result.state).toBe('failed');
  });

  it('errorSummary/abortReason は省略可能（undefined でもクラッシュしない）', () => {
    expect(() => finalizeStatusFromJob({ status: 'failed' })).not.toThrow();
    const result = finalizeStatusFromJob({ status: 'failed' });
    expect(result.state).toBe('failed');
  });
});
