import { describe, it, expect, vi } from 'vitest';
import { executePipeline, type ExecuteOptions } from '../src/proxymodel/executor.js';
import type { ProxyModelDefinition } from '../src/proxymodel/schema.js';
import type { LLMProvider, CacheProvider, StageLogger } from '../src/proxymodel/types.js';

function mockLlm(available = false): LLMProvider {
  return {
    async complete(prompt) { return `Summary: ${prompt.slice(0, 30)}...`; },
    available: () => available,
  };
}

function mockCache(): CacheProvider {
  const store = new Map<string, string>();
  return {
    async getOrCompute(key, compute) {
      if (store.has(key)) return store.get(key)!;
      const val = await compute();
      store.set(key, val);
      return val;
    },
    hash(content) { return content.slice(0, 8); },
    async get(key) { return store.get(key) ?? null; },
    async set(key, value) { store.set(key, value); },
  };
}

function mockLog(): StageLogger {
  return {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  };
}

function makeModel(stages: ProxyModelDefinition['spec']['stages'], appliesTo = ['toolResult'] as const): ProxyModelDefinition {
  return {
    kind: 'ProxyModel',
    metadata: { name: 'test' },
    spec: {
      controller: 'gate',
      stages,
      appliesTo: [...appliesTo],
      cacheable: false,
    },
    source: 'built-in',
  };
}

function makeOpts(content: string, model: ProxyModelDefinition, overrides: Partial<ExecuteOptions> = {}): ExecuteOptions {
  return {
    content,
    contentType: 'toolResult',
    sourceName: 'test/tool',
    projectName: 'test',
    sessionId: 'sess-1',
    proxyModel: model,
    llm: mockLlm(),
    cache: mockCache(),
    ...overrides,
  };
}

describe('executePipeline', () => {
  it('passes content through passthrough stage unchanged', async () => {
    const model = makeModel([{ type: 'passthrough' }]);
    const result = await executePipeline(makeOpts('hello world', model));
    expect(result.content).toBe('hello world');
    expect(result.sections).toBeUndefined();
  });

  it('chains multiple stages', async () => {
    // passthrough → passthrough should still return same content
    const model = makeModel([
      { type: 'passthrough' },
      { type: 'passthrough' },
    ]);
    const result = await executePipeline(makeOpts('data', model));
    expect(result.content).toBe('data');
  });

  it('paginate splits large content', async () => {
    const model = makeModel([
      { type: 'paginate', config: { pageSize: 50 } },
    ]);
    const content = 'line\n'.repeat(100);
    const result = await executePipeline(makeOpts(content, model));
    expect(result.sections).toBeDefined();
    expect(result.sections!.length).toBeGreaterThan(1);
    expect(result.content).toContain('pages');
  });

  it('skips missing stages with warning', async () => {
    const log = mockLog();
    const model = makeModel([
      { type: 'nonexistent-stage' },
      { type: 'passthrough' },
    ]);
    const result = await executePipeline(makeOpts('data', model, { log }));
    expect(result.content).toBe('data');
    expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('nonexistent-stage'));
  });

  it('continues pipeline on stage error', async () => {
    // We'll test this by verifying the pipeline doesn't throw even if something goes wrong internally
    const model = makeModel([
      { type: 'passthrough' },
    ]);
    const result = await executePipeline(makeOpts('data', model));
    expect(result.content).toBe('data');
  });

  it('preserves originalContent across all stages', async () => {
    // section-split + summarize-tree pipeline — originalContent should always be the initial input
    const model = makeModel([
      { type: 'section-split', config: { minSectionSize: 5 } },
    ]);
    const jsonContent = JSON.stringify([
      { id: 'a', label: 'First', data: 'x'.repeat(100) },
      { id: 'b', label: 'Second', data: 'y'.repeat(100) },
    ]);
    const result = await executePipeline(makeOpts(jsonContent, model));
    expect(result.sections).toBeDefined();
  });

  it('respects appliesTo filter', async () => {
    const model = makeModel(
      [{ type: 'passthrough' }],
      ['resource'],
    );
    // contentType is toolResult but model only applies to resource
    const result = await executePipeline(makeOpts('data', model));
    expect(result.content).toBe('data');
    expect(result.sections).toBeUndefined();
  });

  it('returns empty metadata when no stages set it', async () => {
    const model = makeModel([{ type: 'passthrough' }]);
    const result = await executePipeline(makeOpts('data', model));
    expect(result.metadata).toBeUndefined();
  });

  it('handles section-split + summarize-tree (subindex) pipeline', async () => {
    const model = makeModel([
      { type: 'section-split', config: { minSectionSize: 5 } },
      { type: 'summarize-tree' },
    ]);
    const items = Array.from({ length: 10 }, (_, i) => ({
      id: `item-${i}`,
      name: `Item ${i}`,
      data: 'x'.repeat(300),
    }));
    const json = JSON.stringify(items);
    const result = await executePipeline(makeOpts(json, model));
    // Should produce sections
    expect(result.sections).toBeDefined();
    expect(result.content).toContain('sections');
  });

  it('works with empty content', async () => {
    const model = makeModel([{ type: 'passthrough' }]);
    const result = await executePipeline(makeOpts('', model));
    expect(result.content).toBe('');
  });

  describe('audit event emission', () => {
    const mockCollector = { emit: vi.fn(), flush: vi.fn(), dispose: vi.fn() };

    it('emits stage_execution for each stage + pipeline_execution summary', async () => {
      mockCollector.emit.mockClear();
      const model = makeModel([
        { type: 'passthrough' },
        { type: 'paginate', config: { pageSize: 50 } },
      ]);
      const content = 'line\n'.repeat(100);
      await executePipeline(makeOpts(content, model, { auditCollector: mockCollector as never }));

      // 2 stages + 1 pipeline summary = 3 events
      expect(mockCollector.emit).toHaveBeenCalledTimes(3);

      const calls = mockCollector.emit.mock.calls.map((c: unknown[]) => c[0] as { eventKind: string; payload: Record<string, unknown> });
      expect(calls[0]!.eventKind).toBe('stage_execution');
      expect(calls[0]!.payload['stage']).toBe('passthrough');
      expect(calls[0]!.payload['durationMs']).toBeGreaterThanOrEqual(0);
      expect(calls[1]!.eventKind).toBe('stage_execution');
      expect(calls[1]!.payload['stage']).toBe('paginate');
      expect(calls[2]!.eventKind).toBe('pipeline_execution');
      expect(calls[2]!.payload['totalDurationMs']).toBeGreaterThanOrEqual(0);
      expect(calls[2]!.payload['stageCount']).toBe(2);
    });

    it('includes serverName and correlationId when provided', async () => {
      mockCollector.emit.mockClear();
      const model = makeModel([{ type: 'passthrough' }]);
      await executePipeline(makeOpts('hello', model, {
        auditCollector: mockCollector as never,
        serverName: 'ha',
        correlationId: 'req-1',
      }));

      const calls = mockCollector.emit.mock.calls.map((c: unknown[]) => c[0] as { serverName?: string; correlationId?: string });
      for (const call of calls) {
        expect(call.serverName).toBe('ha');
        expect(call.correlationId).toBe('req-1');
      }
    });

    it('does not emit when auditCollector is undefined', async () => {
      mockCollector.emit.mockClear();
      const model = makeModel([{ type: 'passthrough' }]);
      // No auditCollector — should not throw
      await executePipeline(makeOpts('hello', model));
      expect(mockCollector.emit).not.toHaveBeenCalled();
    });

    it('preserves correct inputSize/outputSize per stage', async () => {
      mockCollector.emit.mockClear();
      const model = makeModel([{ type: 'passthrough' }]);
      await executePipeline(makeOpts('hello', model, { auditCollector: mockCollector as never }));

      const stageEvent = mockCollector.emit.mock.calls[0]![0] as { payload: Record<string, unknown> };
      expect(stageEvent.payload['inputSize']).toBe(5);
      expect(stageEvent.payload['outputSize']).toBe(5);
    });

    it('emits pipeline_execution with input/output sizes', async () => {
      mockCollector.emit.mockClear();
      const model = makeModel([{ type: 'passthrough' }]);
      await executePipeline(makeOpts('hello', model, { auditCollector: mockCollector as never }));

      const pipelineEvent = mockCollector.emit.mock.calls[1]![0] as { payload: Record<string, unknown> };
      expect(pipelineEvent.payload['inputSize']).toBe(5);
      expect(pipelineEvent.payload['outputSize']).toBe(5);
      expect(pipelineEvent.payload['stageCount']).toBe(1);
    });
  });
});
