import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { tmpdir } from 'os';
import { LocalProgressReporter } from './local-reporter.js';
import type { Repository } from '../db/repository.js';

describe('LocalProgressReporter.reportPromptPreflight', () => {
  let workspace: string;
  let reporter: LocalProgressReporter;

  beforeEach(() => {
    workspace = fs.mkdtempSync(path.join(tmpdir(), 'preflight-test-'));
    const stubRepo = {} as unknown as Repository;
    reporter = new LocalProgressReporter(stubRepo, 1, workspace);
  });

  afterEach(() => {
    fs.rmSync(workspace, { recursive: true, force: true });
  });

  function readLog(): string {
    const logPath = path.join(workspace, 'logs', 'activity.log');
    return fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : '';
  }

  it('shortens [llm-preflight:ok] to compact "preflight: est/context tokens" form', () => {
    reporter.reportPromptPreflight(
      '[llm-preflight:ok] model=qwen2.5:7b estimated=12,400 safe=102,400 context=128,000 requestJsonChars=...',
    );
    const log = readLog();
    expect(log).toContain('preflight: 12,400/128,000 tokens');
    expect(log).not.toContain('[llm-preflight:ok]');
    expect(log).not.toContain('safe=');
  });

  it('keeps [llm-preflight:blocked] full line for diagnostics', () => {
    const fullLine =
      '[llm-preflight:blocked] model=qwen2.5:7b estimated=300,000 safe=102,400 context=128,000';
    reporter.reportPromptPreflight(fullLine);
    expect(readLog()).toContain(fullLine);
  });

  it('skips ok line that lacks estimated/context (defensive)', () => {
    reporter.reportPromptPreflight('[llm-preflight:ok] malformed');
    const log = readLog();
    expect(log).not.toContain('preflight: ');
    expect(log).not.toContain('[llm-preflight:ok]');
  });
});

describe('LocalProgressReporter.reportToolResult', () => {
  let workspace: string;
  let reporter: LocalProgressReporter;

  beforeEach(() => {
    workspace = fs.mkdtempSync(path.join(tmpdir(), 'tool-result-test-'));
    const stubRepo = {} as unknown as Repository;
    reporter = new LocalProgressReporter(stubRepo, 1, workspace);
  });

  afterEach(() => {
    fs.rmSync(workspace, { recursive: true, force: true });
  });

  function readLog(): string {
    const logPath = path.join(workspace, 'logs', 'activity.log');
    return fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : '';
  }

  it('emits ok line with sub-second precision for fast tools', () => {
    reporter.reportToolResult('Read', { isError: false, durationMs: 23, cacheHit: false });
    expect(readLog()).toContain('Read: ok 23ms');
  });

  it('switches to seconds for ≥1s and tags cache hits separately so they do not look slow', () => {
    reporter.reportToolResult('XPostDetail', { isError: false, durationMs: 132_884, cacheHit: false });
    reporter.reportToolResult('Read', { isError: false, durationMs: 12, cacheHit: true });
    const log = readLog();
    expect(log).toContain('XPostDetail: ok 132.9s');
    expect(log).toContain('Read: ok 12ms (cache)');
  });

  it('reports errors with the same duration format so failed slow tools stand out', () => {
    reporter.reportToolResult('XSearch', { isError: true, durationMs: 90_000, cacheHit: false });
    expect(readLog()).toContain('XSearch: error 90.0s');
  });
});

describe('LocalProgressReporter.reportInterjectionAck', () => {
  let workspace: string;
  let reporter: LocalProgressReporter;

  beforeEach(() => {
    workspace = fs.mkdtempSync(path.join(tmpdir(), 'interjection-test-'));
    const stubRepo = {} as unknown as Repository;
    // isSubTask=true: exercise the activity.log writes but skip the DB comment write
    // (which would need a real Repository).
    reporter = new LocalProgressReporter(stubRepo, 1, workspace, undefined, true);
  });

  afterEach(() => {
    fs.rmSync(workspace, { recursive: true, force: true });
  });

  function readLog(): string {
    const logPath = path.join(workspace, 'logs', 'activity.log');
    return fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : '';
  }

  it('logs the interjection TEXT to activity.log so a resumed agent can read it', () => {
    reporter.reportInterjectionAck(
      [{ id: 7, body: 'やっぱり B を優先して' }],
      'execute',
    );
    const log = readLog();
    expect(log).toContain('interjection: ack 1 message(s)');
    expect(log).toContain('interjection: [#7] やっぱり B を優先して');
  });

  it('truncates long interjection bodies in the log', () => {
    reporter.reportInterjectionAck([{ id: 1, body: 'z'.repeat(700) }], 'execute');
    const log = readLog();
    // truncateLogText(text, 500) keeps 497 chars + '...'
    expect(log).toContain('interjection: [#1] ' + 'z'.repeat(497) + '...');
    expect(log).not.toContain('z'.repeat(700));
  });
});

describe('LocalProgressReporter.reportLLMCall', () => {
  let workspace: string;
  let reporter: LocalProgressReporter;

  beforeEach(() => {
    workspace = fs.mkdtempSync(path.join(tmpdir(), 'llm-call-test-'));
    const stubRepo = {} as unknown as Repository;
    reporter = new LocalProgressReporter(stubRepo, 1, workspace);
  });

  afterEach(() => {
    fs.rmSync(workspace, { recursive: true, force: true });
  });

  function readLog(): string {
    const logPath = path.join(workspace, 'logs', 'activity.log');
    return fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf-8') : '';
  }

  it('reports llm call with tokens and tool count', () => {
    reporter.reportLLMCall({
      durationMs: 2812,
      promptTokens: 32_498,
      completionTokens: 156,
      toolCalls: 1,
      textChars: 42,
    });
    expect(readLog()).toContain('llm: 2.8s in=32498 out=156 tools=1');
  });

  it('falls back to text=Nchars when no tool calls fired', () => {
    reporter.reportLLMCall({
      durationMs: 950,
      promptTokens: 12_000,
      completionTokens: 80,
      toolCalls: 0,
      textChars: 240,
    });
    expect(readLog()).toContain('llm: 950ms in=12000 out=80 text=240chars');
  });

  it('omits token suffix when provider did not report usage', () => {
    reporter.reportLLMCall({
      durationMs: 3500,
      toolCalls: 2,
      textChars: 0,
    });
    expect(readLog()).toContain('llm: 3.5s tools=2');
    expect(readLog()).not.toContain('in=');
  });
});
