import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mcpctlProvider } from '../src/provider';
import { startMockMcpd, type MockMcpd } from './mock-mcpd';

let mock: MockMcpd;
const mcpd = () => ({ url: mock.baseUrl, token: 'tok' });

beforeEach(async () => {
  mock = await startMockMcpd();
});
afterEach(async () => {
  await mock.close();
});

const llmInputs = (over: Record<string, unknown> = {}) => ({
  kind: 'llm',
  name: 'qwen3-thinking',
  spec: { type: 'openai', model: 'qwen3-thinking', url: 'http://litellm:4000', tier: 'heavy' },
  mcpd: mcpd(),
  ...over,
});

describe('check', () => {
  it('accepts a valid llm and applies defaults', async () => {
    const res = await mcpctlProvider.check!(undefined, llmInputs());
    expect(res.failures).toBeUndefined();
    // description/extraConfig/poolName defaults injected.
    expect(res.inputs.spec).toMatchObject({ tier: 'heavy', description: '', extraConfig: {}, poolName: null });
  });

  it('rejects a bad name and missing mcpd', async () => {
    const res = await mcpctlProvider.check!(undefined, { kind: 'llm', name: 'Bad Name', spec: {}, mcpd: { url: '', token: '' } });
    const props = (res.failures ?? []).map((f) => f.property);
    expect(props).toContain('name');
    expect(props).toContain('mcpd');
  });

  it('does not reject unknown kinds (forward-compatible)', async () => {
    const res = await mcpctlProvider.check!(undefined, { kind: 'somenewkind', name: 'x', spec: {}, mcpd: mcpd() });
    expect(res.failures).toBeUndefined();
  });
});

describe('diff', () => {
  const base = { kind: 'llm', name: 'a', mcpd: { url: 'http://x' }, spec: { type: 'openai', model: 'm1' } };

  it('model change → in-place update (changes, no replace)', async () => {
    const r = await mcpctlProvider.diff!('llm/a', base, { ...base, spec: { type: 'openai', model: 'm2' } });
    expect(r.changes).toBe(true);
    expect(r.replaces ?? []).toEqual([]);
  });

  it('name change → replace + deleteBeforeReplace', async () => {
    const r = await mcpctlProvider.diff!('llm/a', base, { ...base, name: 'b' });
    expect(r.replaces).toContain('name');
    expect(r.deleteBeforeReplace).toBe(true);
  });

  it('type change (immutable) → replace on spec.type', async () => {
    const r = await mcpctlProvider.diff!('llm/a', base, { ...base, spec: { type: 'vllm', model: 'm1' } });
    expect(r.replaces).toContain('spec.type');
    expect(r.deleteBeforeReplace).toBe(true);
  });

  it('no change → changes:false', async () => {
    const r = await mcpctlProvider.diff!('llm/a', base, { ...base });
    expect(r.changes).toBe(false);
  });
});

describe('create / read / update / delete round-trip', () => {
  it('create posts, read reflects, update puts, delete removes', async () => {
    const created = await mcpctlProvider.create!(llmInputs());
    expect(created.id).toBe('llm/qwen3-thinking');
    expect(created.outs.resourceId).toMatch(/^cmock/);

    const read = await mcpctlProvider.read!('llm/qwen3-thinking', { ...llmInputs(), ...created.outs });
    expect(read.props?.spec).toMatchObject({ model: 'qwen3-thinking', type: 'openai' });
    expect(read.props?.spec).not.toHaveProperty('name'); // name lives at top level

    const updated = await mcpctlProvider.update!(
      'llm/qwen3-thinking',
      created.outs,
      llmInputs({ spec: { type: 'openai', model: 'qwen3-next', url: 'http://litellm:4000', tier: 'heavy' } }),
    );
    expect(updated.outs.spec.model).toBe('qwen3-next');
    const put = mock.requests.find((r) => r.method === 'PUT')!;
    expect(put.body).not.toHaveProperty('type'); // immutable stripped
    expect(put.body).not.toHaveProperty('name');

    await mcpctlProvider.delete!('llm/qwen3-thinking', created.outs);
    expect([...mock.store['llms']!.values()]).toHaveLength(0);
  });

  it('read returns {} when the row is gone (triggers recreate)', async () => {
    const res = await mcpctlProvider.read!('llm/ghost', { kind: 'llm', name: 'ghost', mcpd: mcpd(), spec: {} });
    expect(res).toEqual({});
  });
});
