import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, mkdirSync, readFileSync, existsSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { TOOL_DEFS, executeTool } from './checklist.js';
import { getToolDefs } from './index.js';
import { buildChecklistContext } from '../piece-runner.js';
import type { ToolContext } from './core.js';

describe('checklist', () => {
  let tempDir: string;
  let ctx: ToolContext;

  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), 'checklist-test-'));
    mkdirSync(join(tempDir, 'logs'), { recursive: true });
    ctx = {
      workspacePath: tempDir,
      editAllowed: true,
    };
  });

  afterEach(() => {
    rmSync(tempDir, { recursive: true, force: true });
  });

  // 1. TOOL_DEFS exports all 3 tools
  it('TOOL_DEFS exports CreateChecklist, CheckItem, GetChecklist', () => {
    expect(TOOL_DEFS).toHaveProperty('CreateChecklist');
    expect(TOOL_DEFS).toHaveProperty('CheckItem');
    expect(TOOL_DEFS).toHaveProperty('GetChecklist');
    expect(Object.keys(TOOL_DEFS)).toHaveLength(3);
  });

  // 2. CreateChecklist creates JSON file correctly
  it('CreateChecklist creates JSON file with correct structure', async () => {
    const result = await executeTool('CreateChecklist', {
      name: 'image-ocr',
      items: [
        { id: 'img_001', label: 'input/img_001.png' },
        { id: 'img_002', label: 'input/img_002.png' },
      ],
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(false);

    const filePath = join(tempDir, 'logs', 'checklists', 'image-ocr.json');
    expect(existsSync(filePath)).toBe(true);

    const data = JSON.parse(readFileSync(filePath, 'utf-8'));
    expect(data.name).toBe('image-ocr');
    expect(data.created_at).toBeTruthy();
    expect(data.updated_at).toBeTruthy();
    expect(data.items).toHaveLength(2);
    expect(data.items[0].id).toBe('img_001');
    expect(data.items[0].label).toBe('input/img_001.png');
    expect(data.items[0].status).toBe('pending');
    expect(data.items[0].result).toBeNull();
    expect(data.items[0].error).toBeNull();
    expect(data.items[0].checked_at).toBeNull();
    expect(data.summary).toEqual({
      total: 2,
      done: 0,
      failed: 0,
      skipped: 0,
      remaining: 2,
    });
  });

  // 3. CreateChecklist rejects duplicate name
  it('CreateChecklist rejects duplicate name', async () => {
    const input = {
      name: 'my-list',
      items: [{ id: 'a', label: 'item a' }],
    };
    await executeTool('CreateChecklist', input, ctx);
    const result = await executeTool('CreateChecklist', input, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('already exists');
  });

  // 4. CreateChecklist rejects invalid name (path traversal)
  it('CreateChecklist rejects path traversal name', async () => {
    const result = await executeTool('CreateChecklist', {
      name: '../etc/passwd',
      items: [{ id: 'a', label: 'x' }],
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
  });

  // 5. CreateChecklist rejects uppercase name
  it('CreateChecklist rejects uppercase name', async () => {
    const result = await executeTool('CreateChecklist', {
      name: 'MyList',
      items: [{ id: 'a', label: 'x' }],
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('Invalid checklist name');
  });

  // 6. CreateChecklist rejects empty items
  it('CreateChecklist rejects empty items', async () => {
    const result = await executeTool('CreateChecklist', {
      name: 'empty-list',
      items: [],
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('non-empty');
  });

  // 7. CheckItem marks item done with result
  it('CheckItem marks item done with result', async () => {
    await executeTool('CreateChecklist', {
      name: 'test-check',
      items: [{ id: 'item1', label: 'first item' }],
    }, ctx);

    const result = await executeTool('CheckItem', {
      name: 'test-check',
      item_id: 'item1',
      status: 'done',
      result: 'OCR completed successfully',
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(false);

    const filePath = join(tempDir, 'logs', 'checklists', 'test-check.json');
    const data = JSON.parse(readFileSync(filePath, 'utf-8'));
    expect(data.items[0].status).toBe('done');
    expect(data.items[0].result).toBe('OCR completed successfully');
    expect(data.items[0].checked_at).toBeTruthy();
    expect(data.summary.done).toBe(1);
    expect(data.summary.remaining).toBe(0);
  });

  // 8. CheckItem marks item failed with error
  it('CheckItem marks item failed with error', async () => {
    await executeTool('CreateChecklist', {
      name: 'fail-test',
      items: [{ id: 'item1', label: 'first item' }],
    }, ctx);

    const result = await executeTool('CheckItem', {
      name: 'fail-test',
      item_id: 'item1',
      status: 'failed',
      error: 'File not found',
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(false);

    const filePath = join(tempDir, 'logs', 'checklists', 'fail-test.json');
    const data = JSON.parse(readFileSync(filePath, 'utf-8'));
    expect(data.items[0].status).toBe('failed');
    expect(data.items[0].error).toBe('File not found');
    expect(data.items[0].checked_at).toBeTruthy();
    expect(data.summary.failed).toBe(1);
    expect(data.summary.remaining).toBe(0);
  });

  // 9. CheckItem marks item skipped
  it('CheckItem marks item skipped', async () => {
    await executeTool('CreateChecklist', {
      name: 'skip-test',
      items: [{ id: 'item1', label: 'first item' }],
    }, ctx);

    const result = await executeTool('CheckItem', {
      name: 'skip-test',
      item_id: 'item1',
      status: 'skipped',
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(false);

    const filePath = join(tempDir, 'logs', 'checklists', 'skip-test.json');
    const data = JSON.parse(readFileSync(filePath, 'utf-8'));
    expect(data.items[0].status).toBe('skipped');
    expect(data.summary.skipped).toBe(1);
    expect(data.summary.remaining).toBe(0);
  });

  // 10. CheckItem rejects unknown item_id
  it('CheckItem rejects unknown item_id', async () => {
    await executeTool('CreateChecklist', {
      name: 'unknown-id',
      items: [{ id: 'item1', label: 'first item' }],
    }, ctx);

    const result = await executeTool('CheckItem', {
      name: 'unknown-id',
      item_id: 'nonexistent',
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('not found');
  });

  // 11. CheckItem rejects nonexistent checklist
  it('CheckItem rejects nonexistent checklist', async () => {
    const result = await executeTool('CheckItem', {
      name: 'no-such-list',
      item_id: 'item1',
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('not found');
  });

  // 12. executeTool returns null for unknown tool name
  it('executeTool returns null for unknown tool name', async () => {
    const result = await executeTool('NonExistentTool', {}, ctx);
    expect(result).toBeNull();
  });

  // 13. GetChecklist returns full state as JSON
  it('GetChecklist returns full state as JSON', async () => {
    await executeTool('CreateChecklist', {
      name: 'get-test',
      items: [
        { id: 'a', label: 'item a' },
        { id: 'b', label: 'item b' },
      ],
    }, ctx);

    await executeTool('CheckItem', {
      name: 'get-test',
      item_id: 'a',
      status: 'done',
      result: 'ok',
    }, ctx);

    const result = await executeTool('GetChecklist', { name: 'get-test' }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(false);

    const data = JSON.parse(result!.output);
    expect(data.name).toBe('get-test');
    expect(data.items).toHaveLength(2);
    expect(data.items[0].status).toBe('done');
    expect(data.items[0].result).toBe('ok');
    expect(data.items[1].status).toBe('pending');
    expect(data.summary.total).toBe(2);
    expect(data.summary.done).toBe(1);
    expect(data.summary.remaining).toBe(1);
  });

  // 14. GetChecklist rejects nonexistent checklist
  it('GetChecklist rejects nonexistent checklist', async () => {
    const result = await executeTool('GetChecklist', { name: 'no-such-list' }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('not found');
  });

  // 15. CheckItem rejects invalid name (uppercase)
  it('CheckItem rejects invalid name (uppercase)', async () => {
    const result = await executeTool('CheckItem', {
      name: 'MyList',
      item_id: 'item1',
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('Invalid checklist name');
  });

  // 16. CheckItem rejects invalid name (path traversal)
  it('CheckItem rejects invalid name (path traversal)', async () => {
    const result = await executeTool('CheckItem', {
      name: '../etc/passwd',
      item_id: 'item1',
    }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('Invalid checklist name');
  });

  // 17. GetChecklist rejects invalid name (uppercase)
  it('GetChecklist rejects invalid name (uppercase)', async () => {
    const result = await executeTool('GetChecklist', { name: 'MyList' }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('Invalid checklist name');
  });

  // 18. GetChecklist rejects invalid name (path traversal)
  it('GetChecklist rejects invalid name (path traversal)', async () => {
    const result = await executeTool('GetChecklist', { name: '../etc/passwd' }, ctx);

    expect(result).not.toBeNull();
    expect(result!.isError).toBe(true);
    expect(result!.output).toContain('Invalid checklist name');
  });

  // 19. writeChecklist encapsulates updated_at and summary updates
  it('writeChecklist encapsulates updated_at and summary updates', async () => {
    await executeTool('CreateChecklist', {
      name: 'encap-test',
      items: [{ id: 'item1', label: 'first item' }],
    }, ctx);

    const filePath = join(tempDir, 'logs', 'checklists', 'encap-test.json');
    const beforeData = JSON.parse(readFileSync(filePath, 'utf-8'));
    const beforeTimestamp = beforeData.updated_at;

    // Wait a bit to ensure timestamp difference
    await new Promise(resolve => setTimeout(resolve, 50));

    // CheckItem should update timestamp without caller having to do it
    await executeTool('CheckItem', {
      name: 'encap-test',
      item_id: 'item1',
      status: 'done',
    }, ctx);

    const afterData = JSON.parse(readFileSync(filePath, 'utf-8'));
    expect(afterData.updated_at).not.toBe(beforeTimestamp);
    expect(afterData.summary.done).toBe(1);
    expect(afterData.summary.remaining).toBe(0);
  });

  // --- buildChecklistContext tests ---

  describe('buildChecklistContext', () => {
    // Test 1: returns empty string when no checklists exist
    it('returns empty string when no checklists exist', () => {
      const result = buildChecklistContext(tempDir);
      expect(result).toBe('');
    });

    // Test 2: generates summary text from checklist files
    it('generates summary text from checklist files', () => {
      const checklistsDir = join(tempDir, 'logs', 'checklists');
      mkdirSync(checklistsDir, { recursive: true });

      const checklistData = {
        name: 'test-checklist',
        created_at: '2024-01-01T00:00:00Z',
        updated_at: '2024-01-01T12:00:00Z',
        items: [
          { id: 'a', label: 'Item A', status: 'done', result: 'ok', error: null, checked_at: '2024-01-01T10:00:00Z' },
          { id: 'b', label: 'Item B', status: 'failed', result: null, error: 'timeout', checked_at: '2024-01-01T11:00:00Z' },
          { id: 'c', label: 'Item C', status: 'pending', result: null, error: null, checked_at: null },
        ],
        summary: {
          total: 3,
          done: 1,
          failed: 1,
          skipped: 0,
          remaining: 1,
        },
      };

      writeFileSync(join(checklistsDir, 'test-checklist.json'), JSON.stringify(checklistData));

      const result = buildChecklistContext(tempDir);

      expect(result).toContain('## 作業チェックシート');
      expect(result).toContain('test-checklist');
      expect(result).toContain('1/3完了');
      expect(result).toContain('残りアイテム: c');
      expect(result).toContain('失敗アイテム: b (error: timeout)');
    });

    // Test 3: limits to 5 checklists sorted by updated_at
    it('limits to 5 checklists sorted by updated_at', () => {
      const checklistsDir = join(tempDir, 'logs', 'checklists');
      mkdirSync(checklistsDir, { recursive: true });

      // Create 7 checklist files with different updated_at timestamps
      for (let i = 1; i <= 7; i++) {
        const checklistData = {
          name: `checklist-${i}`,
          created_at: '2024-01-01T00:00:00Z',
          updated_at: `2024-01-01T${String(i).padStart(2, '0')}:00:00Z`,
          items: [{ id: 'item1', label: 'Item 1', status: 'pending', result: null, error: null, checked_at: null }],
          summary: {
            total: 1,
            done: 0,
            failed: 0,
            skipped: 0,
            remaining: 1,
          },
        };
        writeFileSync(join(checklistsDir, `checklist-${i}.json`), JSON.stringify(checklistData));
      }

      const result = buildChecklistContext(tempDir);

      // Only 5 newest (by updated_at) should appear
      expect(result).toContain('checklist-7');
      expect(result).toContain('checklist-6');
      expect(result).toContain('checklist-5');
      expect(result).toContain('checklist-4');
      expect(result).toContain('checklist-3');
      // 2 oldest should not appear
      expect(result).not.toContain('checklist-2');
      expect(result).not.toContain('checklist-1');
    });
  });
});

describe('checklist tools as META_TOOLS', () => {
  it('getToolDefs([]) auto-includes CreateChecklist / CheckItem / GetChecklist', async () => {
    const defs = await getToolDefs([], false, { vlmEnabled: false });
    const names = defs.map((d) => d.function.name);
    expect(names).toContain('CreateChecklist');
    expect(names).toContain('CheckItem');
    expect(names).toContain('GetChecklist');
    expect(names).toContain('ReadToolDoc');
  });

  it('does not duplicate when piece already lists checklist tools', async () => {
    const defs = await getToolDefs(['CreateChecklist', 'Read'], false, { vlmEnabled: false });
    const createCount = defs.filter((d) => d.function.name === 'CreateChecklist').length;
    expect(createCount).toBe(1);
  });
});
