import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createLocalModelSet } from '../arena-local-models.js';
import { loadLlamaProviders } from '../llama-providers.js';

const log = { warn: vi.fn() };

function loadFixture(providers: Array<{ id: string; label: string; baseUrl: string }>): void {
  const file = {
    defaultProvider: providers[0]!.id,
    providers: providers.map((p) => ({ ...p, kind: 'llama-swap' })),
  };
  const path = join(tmpdir(), `llama-providers-alm-${Math.random().toString(36).slice(2)}.json`);
  writeFileSync(path, JSON.stringify(file), 'utf8');
  loadLlamaProviders(path, 'http://legacy.test:8080');
}

function modelsResponse(ids: string[]): Response {
  return new Response(JSON.stringify({ data: ids.map((id) => ({ id })) }), {
    status: 200,
    headers: { 'content-type': 'application/json' },
  });
}

describe('createLocalModelSet', () => {
  const fetchMock = vi.fn();

  beforeEach(() => {
    vi.stubGlobal('fetch', fetchMock);
    fetchMock.mockReset();
    log.warn.mockReset();
    loadFixture([
      { id: 'sam-desktop', label: 'Sam Desktop', baseUrl: 'http://a.test:8401' },
      { id: 'embedding', label: 'Embedding', baseUrl: 'http://b.test:8411' },
    ]);
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('adds composite ids from every provider, bare ids only from the default', async () => {
    fetchMock.mockImplementation((url: string) =>
      url.startsWith('http://a.test')
        ? Promise.resolve(modelsResponse(['qwen3.6-35b']))
        : Promise.resolve(modelsResponse(['gemma-4-12b'])),
    );
    const handle = createLocalModelSet(log);
    await handle.refresh();
    expect(handle.set.has('sam-desktop/qwen3.6-35b')).toBe(true);
    expect(handle.set.has('embedding/gemma-4-12b')).toBe(true);
    expect(handle.set.has('qwen3.6-35b')).toBe(true); // bare from default
    expect(handle.set.has('gemma-4-12b')).toBe(false); // bare NOT from non-default
  });

  it('keeps last-known contribution when a provider goes unreachable, drops removed models when reachable', async () => {
    fetchMock.mockImplementation((url: string) =>
      url.startsWith('http://a.test')
        ? Promise.resolve(modelsResponse(['qwen3.6-35b', 'old-model']))
        : Promise.resolve(modelsResponse(['gemma-4-12b'])),
    );
    const handle = createLocalModelSet(log);
    await handle.refresh();
    expect(handle.set.has('sam-desktop/old-model')).toBe(true);

    // Second refresh: provider A drops a model, provider B is down.
    fetchMock.mockImplementation((url: string) =>
      url.startsWith('http://a.test')
        ? Promise.resolve(modelsResponse(['qwen3.6-35b']))
        : Promise.reject(new Error('ECONNREFUSED')),
    );
    await handle.refresh();
    expect(handle.set.has('sam-desktop/old-model')).toBe(false); // removed on reachable provider
    expect(handle.set.has('embedding/gemma-4-12b')).toBe(true); // kept for unreachable provider
    expect(log.warn).toHaveBeenCalled();
  });

  it('recovers a provider that was down at first refresh', async () => {
    fetchMock.mockImplementation((url: string) =>
      url.startsWith('http://a.test')
        ? Promise.resolve(modelsResponse(['qwen3.6-35b']))
        : Promise.reject(new Error('ECONNREFUSED')),
    );
    const handle = createLocalModelSet(log);
    await handle.refresh();
    expect(handle.set.has('embedding/gemma-4-12b')).toBe(false);

    fetchMock.mockImplementation((url: string) =>
      url.startsWith('http://a.test')
        ? Promise.resolve(modelsResponse(['qwen3.6-35b']))
        : Promise.resolve(modelsResponse(['gemma-4-12b'])),
    );
    await handle.refresh();
    expect(handle.set.has('embedding/gemma-4-12b')).toBe(true);
  });
});
