import { describe, it, expect, vi, beforeEach } from 'vitest';

const mockEnsureReady = vi.fn(async () => {});
const mockPrompt = vi.fn(async () => 'mock response');
const mockDispose = vi.fn();

vi.mock('../src/providers/acp-client.js', () => ({
  AcpClient: vi.fn(function (this: Record<string, unknown>) {
    this.ensureReady = mockEnsureReady;
    this.prompt = mockPrompt;
    this.dispose = mockDispose;
  }),
}));

// Must import after mock setup
const { GeminiAcpProvider } = await import('../src/providers/gemini-acp.js');

describe('GeminiAcpProvider', () => {
  let provider: InstanceType<typeof GeminiAcpProvider>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockPrompt.mockResolvedValue('mock response');
    provider = new GeminiAcpProvider({ binaryPath: '/usr/bin/gemini', defaultModel: 'gemini-2.5-flash' });
  });

  describe('complete', () => {
    it('builds prompt from messages and returns CompletionResult', async () => {
      mockPrompt.mockResolvedValueOnce('The answer is 42.');

      const result = await provider.complete({
        messages: [
          { role: 'system', content: 'You are helpful.' },
          { role: 'user', content: 'What is the answer?' },
        ],
      });

      expect(result.content).toBe('The answer is 42.');
      expect(result.toolCalls).toEqual([]);
      expect(result.finishReason).toBe('stop');

      const promptText = mockPrompt.mock.calls[0][0] as string;
      expect(promptText).toContain('System: You are helpful.');
      expect(promptText).toContain('What is the answer?');
    });

    it('formats assistant messages with prefix', async () => {
      mockPrompt.mockResolvedValueOnce('ok');

      await provider.complete({
        messages: [
          { role: 'user', content: 'Hello' },
          { role: 'assistant', content: 'Hi there' },
          { role: 'user', content: 'How are you?' },
        ],
      });

      const promptText = mockPrompt.mock.calls[0][0] as string;
      expect(promptText).toContain('Assistant: Hi there');
    });

    it('trims response content', async () => {
      mockPrompt.mockResolvedValueOnce('  padded response  \n');

      const result = await provider.complete({
        messages: [{ role: 'user', content: 'test' }],
      });

      expect(result.content).toBe('padded response');
    });

    it('serializes concurrent calls to same model', async () => {
      const callOrder: number[] = [];
      let callCount = 0;

      mockPrompt.mockImplementation(async () => {
        const myCall = ++callCount;
        callOrder.push(myCall);
        await new Promise((r) => setTimeout(r, 10));
        return `response-${myCall}`;
      });

      const [r1, r2, r3] = await Promise.all([
        provider.complete({ messages: [{ role: 'user', content: 'a' }] }),
        provider.complete({ messages: [{ role: 'user', content: 'b' }] }),
        provider.complete({ messages: [{ role: 'user', content: 'c' }] }),
      ]);

      expect(r1.content).toBe('response-1');
      expect(r2.content).toBe('response-2');
      expect(r3.content).toBe('response-3');
      expect(callOrder).toEqual([1, 2, 3]);
    });

    it('continues queue after error', async () => {
      mockPrompt
        .mockRejectedValueOnce(new Error('first fails'))
        .mockResolvedValueOnce('second works');

      const results = await Promise.allSettled([
        provider.complete({ messages: [{ role: 'user', content: 'a' }] }),
        provider.complete({ messages: [{ role: 'user', content: 'b' }] }),
      ]);

      expect(results[0].status).toBe('rejected');
      expect(results[1].status).toBe('fulfilled');
      if (results[1].status === 'fulfilled') {
        expect(results[1].value.content).toBe('second works');
      }
    });
  });

  describe('session pool', () => {
    it('creates separate pool entries for different models', async () => {
      mockPrompt.mockResolvedValue('ok');

      await provider.complete({ messages: [{ role: 'user', content: 'a' }], model: 'gemini-2.5-flash' });
      await provider.complete({ messages: [{ role: 'user', content: 'b' }], model: 'gemini-2.5-pro' });

      expect(provider.poolSize).toBe(2);
    });

    it('reuses existing pool entry for same model', async () => {
      mockPrompt.mockResolvedValue('ok');

      await provider.complete({ messages: [{ role: 'user', content: 'a' }], model: 'gemini-2.5-flash' });
      await provider.complete({ messages: [{ role: 'user', content: 'b' }], model: 'gemini-2.5-flash' });

      expect(provider.poolSize).toBe(1);
    });

    it('uses defaultModel when no model specified', async () => {
      mockPrompt.mockResolvedValue('ok');

      await provider.complete({ messages: [{ role: 'user', content: 'a' }] });

      expect(provider.poolSize).toBe(1);
    });

    it('evicts idle sessions', async () => {
      // Use a very short TTL for testing
      const shortTtl = new GeminiAcpProvider({
        binaryPath: '/usr/bin/gemini',
        defaultModel: 'gemini-2.5-flash',
        idleTtlMs: 1, // 1ms TTL
      });

      mockPrompt.mockResolvedValue('ok');
      await shortTtl.complete({ messages: [{ role: 'user', content: 'a' }], model: 'model-a' });
      expect(shortTtl.poolSize).toBe(1);

      // Wait for TTL to expire
      await new Promise((r) => setTimeout(r, 10));

      // Next complete call triggers eviction of old entry and creates new one
      await shortTtl.complete({ messages: [{ role: 'user', content: 'b' }], model: 'model-b' });
      // model-a should have been evicted, only model-b remains
      expect(shortTtl.poolSize).toBe(1);
      expect(mockDispose).toHaveBeenCalled();

      shortTtl.dispose();
    });

    it('dispose kills all pooled clients', async () => {
      mockPrompt.mockResolvedValue('ok');

      await provider.complete({ messages: [{ role: 'user', content: 'a' }], model: 'model-a' });
      await provider.complete({ messages: [{ role: 'user', content: 'b' }], model: 'model-b' });
      expect(provider.poolSize).toBe(2);

      provider.dispose();
      expect(provider.poolSize).toBe(0);
      expect(mockDispose).toHaveBeenCalledTimes(2);
    });
  });

  describe('listModels', () => {
    it('returns static model list', async () => {
      const models = await provider.listModels();
      expect(models).toContain('gemini-2.5-flash');
      expect(models).toContain('gemini-2.5-pro');
      expect(models).toContain('gemini-2.0-flash');
    });
  });

  describe('dispose', () => {
    it('delegates to all pooled AcpClients', async () => {
      mockPrompt.mockResolvedValue('ok');
      await provider.complete({ messages: [{ role: 'user', content: 'test' }] });
      provider.dispose();
      expect(mockDispose).toHaveBeenCalled();
    });
  });

  describe('name', () => {
    it('is gemini-cli for config compatibility', () => {
      expect(provider.name).toBe('gemini-cli');
    });
  });
});
