import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { tmpdir } from 'os';
import { executeCoreTools, type ToolContext, type FileProvenanceEvent } from './core.js';
import { Repository } from '../../db/repository.js';

function makeWs(): string {
  const ws = fs.mkdtempSync(path.join(tmpdir(), 'maestro-core-prov-'));
  fs.mkdirSync(path.join(ws, 'output'), { recursive: true });
  return ws;
}

describe('core tools record file provenance (best-effort)', () => {
  let ws = '';
  let events: FileProvenanceEvent[] = [];
  let ctx: ToolContext;

  beforeEach(() => {
    ws = makeWs();
    events = [];
    ctx = {
      workspacePath: ws,
      editAllowed: true,
      bashSandbox: 'off',
      bashUnrestricted: true,
      recordFileProvenance: (ev) => events.push(ev),
    } as ToolContext;
  });

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

  it('Write new file records a create/agent_output event', async () => {
    await executeCoreTools('Write', { file_path: 'output/new.txt', content: 'hi' }, ctx);
    expect(events).toEqual([{ relPath: 'output/new.txt', event: 'create', sourceKind: 'agent_output' }]);
  });

  it('Write over an existing file records a modify event', async () => {
    fs.writeFileSync(path.join(ws, 'output', 'exists.txt'), 'old');
    await executeCoreTools('Write', { file_path: 'output/exists.txt', content: 'new' }, ctx);
    expect(events).toEqual([{ relPath: 'output/exists.txt', event: 'modify', sourceKind: 'agent_output' }]);
  });

  it('Edit records a modify event (preserving creator upstream)', async () => {
    fs.writeFileSync(path.join(ws, 'output', 'edit.txt'), 'hello world');
    await executeCoreTools('Edit', { file_path: 'output/edit.txt', old_string: 'world', new_string: 'there' }, ctx);
    expect(events).toEqual([{ relPath: 'output/edit.txt', event: 'modify', sourceKind: 'agent_edit' }]);
  });

  it('never breaks tool execution when the recorder throws', async () => {
    ctx.recordFileProvenance = vi.fn(() => { throw new Error('boom'); });
    const res = await executeCoreTools('Write', { file_path: 'output/ok.txt', content: 'x' }, ctx);
    expect(res!.isError).toBe(false);
    expect(fs.readFileSync(path.join(ws, 'output', 'ok.txt'), 'utf8')).toBe('x');
  });

  it('Bash detects a newly created output/ file as bash_generated', async () => {
    await executeCoreTools('Bash', { command: 'echo hi > output/made.txt' }, ctx);
    const made = events.filter((e) => e.relPath === 'output/made.txt');
    expect(made).toHaveLength(1);
    expect(made[0]).toMatchObject({ event: 'create', sourceKind: 'bash_generated' });
  });

  it('Bash detects a changed output/ file as modify', async () => {
    fs.writeFileSync(path.join(ws, 'output', 'pre.txt'), 'v1');
    // ensure a distinct mtime
    const past = new Date(Date.now() - 5000);
    fs.utimesSync(path.join(ws, 'output', 'pre.txt'), past, past);
    await executeCoreTools('Bash', { command: 'echo v2 >> output/pre.txt' }, ctx);
    const changed = events.filter((e) => e.relPath === 'output/pre.txt');
    expect(changed).toHaveLength(1);
    expect(changed[0]!.event).toBe('modify');
  });

  it('does nothing when no recorder is bound', async () => {
    const noRec = { workspacePath: ws, editAllowed: true } as ToolContext;
    const res = await executeCoreTools('Write', { file_path: 'output/x.txt', content: 'y' }, noRec);
    expect(res!.isError).toBe(false);
  });

  // Review D1/#4: a Bash command that does not touch an existing output/ file
  // must emit NO event for it (guards against false-modify spam).
  it('Bash emits no event for an untouched existing output/ file', async () => {
    fs.writeFileSync(path.join(ws, 'output', 'untouched.txt'), 'stable');
    const past = new Date(Date.now() - 5000);
    fs.utimesSync(path.join(ws, 'output', 'untouched.txt'), past, past);
    await executeCoreTools('Bash', { command: 'echo hi > output/other.txt' }, ctx);
    expect(events.filter((e) => e.relPath === 'output/untouched.txt')).toHaveLength(0);
  });
});

// Review #2: the split unit tests never wire core's emitted event to a REAL
// Repository, so a field-mapping bug in the worker glue would go unnoticed.
// Exercise the round trip: executeCoreTools emits → recorder forwards to
// repo.recordProvenance → getProvenance returns the persisted record.
describe('core recording glue → real Repository round trip', () => {
  it('a Write recorded through repo.recordProvenance is queryable with the right creator/kind', async () => {
    const ws = makeWs();
    const repo = new Repository(path.join(ws, 'prov.db'));
    const ctx = {
      workspacePath: ws, editAllowed: true,
      recordFileProvenance: (ev: FileProvenanceEvent) =>
        repo.recordProvenance({ workspacePath: ws, taskId: 7, jobId: 'j7', piece: 'chat', movement: 'respond', ...ev }),
    } as ToolContext;

    await executeCoreTools('Write', { file_path: 'output/z.txt', content: 'hi' }, ctx);
    const rec = repo.getProvenance(ws, 'output/z.txt');
    expect(rec).not.toBeNull();
    expect(rec!.createdByTaskId).toBe(7);
    expect(rec!.sourceKind).toBe('agent_output');
    expect(rec!.createdByPiece).toBe('chat');
    fs.rmSync(ws, { recursive: true, force: true });
  });
});
