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

// Control the mock return values from tests.
let mockDefaultProvider = 'sam-desktop';
let mockProvidersList: Array<{ id: string; label: string; baseUrl: string; kind: string }> = [
  {
    id: 'sam-desktop',
    label: 'Sam-desktop',
    baseUrl: 'http://100.101.41.16:8401',
    kind: 'llama-swap',
  },
  {
    id: 'embedding',
    label: 'embedding',
    baseUrl: 'http://100.90.172.55:8411',
    kind: 'llama-swap',
  },
];

vi.mock('../llama-providers.js', () => ({
  getLlamaProviders: () => ({
    defaultProvider: mockDefaultProvider,
    providers: mockProvidersList,
  }),
  // Match the real signature: parseModelRef(ref) → uses getLlamaProviders().defaultProvider internally.
  parseModelRef: (ref: string) => {
    const slashIdx = ref.indexOf('/');
    if (slashIdx <= 0) {
      return { providerId: mockDefaultProvider, wireModelId: ref, isLegacyBareId: true };
    }
    return {
      providerId: ref.slice(0, slashIdx),
      wireModelId: ref.slice(slashIdx + 1),
      isLegacyBareId: false,
    };
  },
}));

// Import the functions under test AFTER the mock is registered.
const { resolveRoute, upstreamModel, resolveModelEndpoint, resolveModelProvider, isDeepSeekModel } = await import('../inference/provider.js');

beforeEach(() => {
  mockDefaultProvider = 'sam-desktop';
  mockProvidersList = [
    {
      id: 'sam-desktop',
      label: 'Sam-desktop',
      baseUrl: 'http://100.101.41.16:8401',
      kind: 'llama-swap',
    },
    {
      id: 'embedding',
      label: 'embedding',
      baseUrl: 'http://100.90.172.55:8411',
      kind: 'llama-swap',
    },
  ];
});

// ---------------------------------------------------------------------------
// Legacy resolveRoute backward compat
// ---------------------------------------------------------------------------

describe('resolveRoute (legacy compat)', () => {
  it('routes to swap when agent is null', () => {
    expect(resolveRoute(null, { LLAMA_SWAP_URL: 'http://localhost:8080' }, 'model')).toEqual({ route: 'swap' });
  });

  it('routes to deepseek for bare deepseek- prefix when configured', () => {
    expect(
      resolveRoute(null, { LLAMA_SWAP_URL: 'http://localhost:8080', DEEPSEEK_API_KEY: 'sk-123' }, 'deepseek-v4-pro'),
    ).toEqual({ route: 'deepseek' });
  });
});

// ---------------------------------------------------------------------------
// Provider-aware resolver: composite ids
// ---------------------------------------------------------------------------

describe('resolveModelProvider', () => {
  const config = {
    LLAMA_SWAP_URL: 'http://localhost:8080',
    DEEPSEEK_API_KEY: 'sk-test',
    DEEPSEEK_BASE_URL: 'https://api.deepseek.com',
  };

  it('routes composite local provider id to its baseUrl', () => {
    const r = resolveModelProvider('sam-desktop/qwen3.6-35b-a3b', config);
    expect(r.route).toBe('swap');
    expect(r.baseUrl).toBe('http://100.101.41.16:8401');
    expect(r.wireModelId).toBe('qwen3.6-35b-a3b');
    expect(r.providerId).toBe('sam-desktop');
    expect(r.isLegacyBareId).toBe(false);
  });

  it('routes composite "deepseek/" id to DeepSeek SDK', () => {
    const r = resolveModelProvider('deepseek/deepseek-v4-pro', config);
    expect(r.route).toBe('deepseek');
    expect(r.baseUrl).toBe('https://api.deepseek.com');
    expect(r.wireModelId).toBe('deepseek-v4-pro');
    expect(r.providerId).toBe('deepseek');
  });

  // COLLISION CASE: "embedding/deepseek-r1-qwen3-8b" routes to local provider
  // "embedding", NOT to DeepSeek cloud.
  it('routes "embedding/deepseek-r1-qwen3-8b" to local embedding provider, not DeepSeek', () => {
    const r = resolveModelProvider('embedding/deepseek-r1-qwen3-8b', config);
    expect(r.route).toBe('swap');
    expect(r.baseUrl).toBe('http://100.90.172.55:8411');
    expect(r.wireModelId).toBe('deepseek-r1-qwen3-8b');
    expect(r.providerId).toBe('embedding');
  });
});

// ---------------------------------------------------------------------------
// Provider-aware resolver: bare (legacy) ids
// ---------------------------------------------------------------------------

describe('resolveModelProvider — bare id legacy fallback', () => {
  const config = {
    LLAMA_SWAP_URL: 'http://localhost:8080',
    DEEPSEEK_API_KEY: 'sk-test',
  };

  it('bare id resolves through defaultProvider', () => {
    const r = resolveModelProvider('qwen3.6-35b-a3b', config);
    expect(r.route).toBe('swap');
    expect(r.providerId).toBe('sam-desktop');
    expect(r.wireModelId).toBe('qwen3.6-35b-a3b');
    expect(r.isLegacyBareId).toBe(true);
  });

  it('bare "deepseek-v4-pro" resolves to DeepSeek SDK (legacy prefix)', () => {
    const r = resolveModelProvider('deepseek-v4-pro', config);
    expect(r.route).toBe('deepseek');
    expect(r.wireModelId).toBe('deepseek-v4-pro');
    expect(r.isLegacyBareId).toBe(true);
  });

  it('bare id when DEEPSEEK_API_KEY is unset stays on swap', () => {
    const r = resolveModelProvider('deepseek-v4-pro', { LLAMA_SWAP_URL: 'http://localhost:8080' });
    expect(r.route).toBe('swap');
    expect(r.wireModelId).toBe('deepseek-v4-pro');
  });

  it('unknown composite provider falls back to LLAMA_SWAP_URL', () => {
    const r = resolveModelProvider('unknown-provider/model-x', config);
    expect(r.route).toBe('swap');
    expect(r.baseUrl).toBe('http://localhost:8080');
    expect(r.wireModelId).toBe('model-x');
    expect(r.isLegacyBareId).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// Anthropic route
// ---------------------------------------------------------------------------

describe('resolveModelProvider — anthropic route', () => {
  const cfg = { LLAMA_SWAP_URL: 'http://localhost:8080', ANTHROPIC_API_KEY: 'sk-ant' };

  it('routes composite "anthropic/" id to the anthropic wire', () => {
    const r = resolveModelProvider('anthropic/claude-opus-4-8', cfg);
    expect(r.route).toBe('anthropic');
    expect(r.providerId).toBe('anthropic');
    expect(r.wireModelId).toBe('claude-opus-4-8');
    expect(r.baseUrl).toBe('https://api.anthropic.com');
  });

  it('routes bare "claude-*" id to anthropic when configured', () => {
    const r = resolveModelProvider('claude-sonnet-4-6', cfg);
    expect(r.route).toBe('anthropic');
    expect(r.wireModelId).toBe('claude-sonnet-4-6');
  });

  it('bare "claude-*" stays on swap when ANTHROPIC_API_KEY is unset', () => {
    const r = resolveModelProvider('claude-opus-4-8', { LLAMA_SWAP_URL: 'http://localhost:8080' });
    expect(r.route).toBe('swap');
  });

  it('honors ANTHROPIC_BASE_URL override and strips trailing slash', () => {
    const r = resolveModelProvider('claude-opus-4-8', {
      ...cfg,
      ANTHROPIC_BASE_URL: 'https://proxy.example.com/',
    });
    expect(r.baseUrl).toBe('https://proxy.example.com');
  });

  it('resolveModelEndpoint throws for the anthropic wire (no OpenAI direct-fetch)', () => {
    expect(() => resolveModelEndpoint(cfg, 'anthropic/claude-opus-4-8')).toThrow(
      /anthropic wire has no OpenAI-compatible direct-fetch endpoint/,
    );
  });
});

// ---------------------------------------------------------------------------
// upstreamModel uses the resolver
// ---------------------------------------------------------------------------

describe('upstreamModel', () => {
  const swapConfig = { LLAMA_SWAP_URL: 'http://localhost:8401' };

  it('returns a model for local composite id', () => {
    const model = upstreamModel(swapConfig, 'sam-desktop/test-model');
    expect(model).toBeDefined();
    expect((model as any).modelId).toBe('test-model');
  });

  it('returns a model for bare id (legacy)', () => {
    const model = upstreamModel(swapConfig, 'test-model');
    expect(model).toBeDefined();
    expect((model as any).modelId).toBe('test-model');
  });
});

// ---------------------------------------------------------------------------
// resolveModelEndpoint uses the resolver
// ---------------------------------------------------------------------------

describe('resolveModelEndpoint', () => {
  it('resolves local composite id to provider baseUrl', () => {
    const ep = resolveModelEndpoint(
      { LLAMA_SWAP_URL: 'http://localhost:8080' },
      'sam-desktop/qwen3.6-35b-a3b',
    );
    expect(ep.url).toBe('http://100.101.41.16:8401');
    expect(ep.model).toBe('qwen3.6-35b-a3b');
    expect(ep.headers['Content-Type']).toBe('application/json');
  });

  it('resolves bare id to default provider baseUrl', () => {
    const ep = resolveModelEndpoint(
      { LLAMA_SWAP_URL: 'http://localhost:8080' },
      'test-model',
    );
    expect(ep.url).toBe('http://100.101.41.16:8401');
    expect(ep.model).toBe('test-model');
  });

  it('resolves deepseek composite id to DeepSeek API with auth header', () => {
    const ep = resolveModelEndpoint(
      { LLAMA_SWAP_URL: 'http://localhost:8080', DEEPSEEK_API_KEY: 'sk-test' },
      'deepseek/deepseek-v4-pro',
    );
    expect(ep.url).toBe('https://api.deepseek.com');
    expect(ep.model).toBe('deepseek-v4-pro');
    expect(ep.headers['Authorization']).toBe('Bearer sk-test');
  });

  // Collision case for endpoint resolution.
  it('resolves "embedding/deepseek-r1-qwen3-8b" to embedding baseUrl, not DeepSeek', () => {
    const ep = resolveModelEndpoint(
      { LLAMA_SWAP_URL: 'http://localhost:8080', DEEPSEEK_API_KEY: 'sk-test' },
      'embedding/deepseek-r1-qwen3-8b',
    );
    expect(ep.url).toBe('http://100.90.172.55:8411');
    expect(ep.model).toBe('deepseek-r1-qwen3-8b');
  });
});

// ---------------------------------------------------------------------------
// isDeepSeekModel (legacy prefix check, kept for stream-phase-adapter)
// ---------------------------------------------------------------------------

describe('isDeepSeekModel', () => {
  it('returns true for deepseek- prefix', () => {
    expect(isDeepSeekModel('deepseek-v4-pro')).toBe(true);
  });

  it('returns false for composite deepseek/', () => {
    expect(isDeepSeekModel('deepseek/deepseek-v4-pro')).toBe(false);
  });

  it('returns false for other models', () => {
    expect(isDeepSeekModel('qwen3.6-35b-a3b')).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// P4: upstreamModel additive source param
// ---------------------------------------------------------------------------

describe('upstreamModel source param (P4)', () => {
  const swapConfig = { LLAMA_SWAP_URL: 'http://localhost:8401' };

  it('accepts optional source parameter without breaking existing calls', () => {
    const model1 = upstreamModel(swapConfig, 'sam-desktop/test-model');
    const model2 = upstreamModel(swapConfig, 'sam-desktop/test-model', undefined, 'boochat');
    expect(model1).toBeDefined();
    expect(model2).toBeDefined();
    expect((model1 as any).modelId).toBe('test-model');
    expect((model2 as any).modelId).toBe('test-model');
  });

  it('creates distinct cached providers for different source values', () => {
    const modelNoSource = upstreamModel(swapConfig, 'sam-desktop/test-model');
    const modelBoochat = upstreamModel(swapConfig, 'sam-desktop/test-model', undefined, 'boochat');
    const modelBoocoder = upstreamModel(swapConfig, 'sam-desktop/test-model', undefined, 'boocoder');
    expect(modelNoSource).toBeDefined();
    expect(modelBoochat).toBeDefined();
    expect(modelBoocoder).toBeDefined();
  });
});

// ---------------------------------------------------------------------------
// P7: gateway routing (auto:* virtual models)
// ---------------------------------------------------------------------------

describe('resolveModelProvider — gateway routing (P7)', () => {
  const config = { LLAMA_SWAP_URL: 'http://localhost:8080' };

  it('routes a known gateway-kind provider to route "gateway"', () => {
    mockProvidersList = [
      ...mockProvidersList,
      { id: 'auto', label: 'Auto (gateway)', baseUrl: 'http://100.114.205.53:9503', kind: 'boocontrol-gateway' },
    ];
    const r = resolveModelProvider('auto/auto:code', config);
    expect(r.route).toBe('gateway');
    expect(r.baseUrl).toBe('http://100.114.205.53:9503');
    expect(r.wireModelId).toBe('auto:code');
    expect(r.providerId).toBe('auto');
  });

  it('resolves an orphaned auto:* session to gateway_error, never swap', () => {
    // No gateway provider in the registry — the entry was removed.
    const r = resolveModelProvider('auto/auto:code', config);
    expect(r.route).toBe('gateway_error');
    expect(r.gatewayReason).toBe('offline');
    expect(r.baseUrl).not.toBe(config.LLAMA_SWAP_URL);
  });

  it('upstreamModel throws a clean error for gateway_error', () => {
    expect(() => upstreamModel(config, 'auto/auto:fast')).toThrow(/routing gateway offline/);
  });

  it('resolveModelEndpoint throws a clean error for gateway_error', () => {
    expect(() => resolveModelEndpoint(config, 'auto/auto:fast')).toThrow(/routing gateway offline/);
  });

  it('upstreamModel returns a model for a live gateway', () => {
    mockProvidersList = [
      ...mockProvidersList,
      { id: 'auto', label: 'Auto (gateway)', baseUrl: 'http://100.114.205.53:9503', kind: 'boocontrol-gateway' },
    ];
    const model = upstreamModel(config, 'auto/auto:code');
    expect(model).toBeDefined();
    expect((model as any).modelId).toBe('auto:code');
  });
});

// ---------------------------------------------------------------------------
// P7 G3: bare auto:* footgun fix
// ---------------------------------------------------------------------------

describe('resolveModelProvider — bare auto:* routing (G3)', () => {
  const config = { LLAMA_SWAP_URL: 'http://localhost:8080' };

  it('bare "auto:code" with a gateway registered routes to gateway (not the default swap host)', () => {
    mockProvidersList = [
      ...mockProvidersList,
      { id: 'auto', label: 'Auto (gateway)', baseUrl: 'http://100.114.205.53:9503', kind: 'boocontrol-gateway' },
    ];
    const r = resolveModelProvider('auto:code', config);
    expect(r.route).toBe('gateway');
    expect(r.baseUrl).toBe('http://100.114.205.53:9503');
    expect(r.wireModelId).toBe('auto:code');
  });

  it('bare "auto:code" with NO gateway resolves to gateway_error, never swap', () => {
    const r = resolveModelProvider('auto:code', config);
    expect(r.route).toBe('gateway_error');
    expect(r.gatewayReason).toBe('offline');
    expect(r.baseUrl).not.toBe(config.LLAMA_SWAP_URL);
  });
});

// ---------------------------------------------------------------------------
// DeepSeek beta endpoint routing (A5)
// ---------------------------------------------------------------------------

describe('upstreamModel — DeepSeek beta routing', () => {
  const dsConfig = {
    LLAMA_SWAP_URL: 'http://localhost:8080',
    DEEPSEEK_API_KEY: 'sk-test',
    DEEPSEEK_BASE_URL: 'https://api.deepseek.com',
    DEEPSEEK_BETA_BASE_URL: 'https://api.deepseek.com/beta',
  };

  it('DeepSeek without useBeta returns a model (stable endpoint)', () => {
    const model = upstreamModel(dsConfig, 'deepseek-v4-pro');
    expect(model).toBeDefined();
    expect((model as any).modelId).toBe('deepseek-v4-pro');
  });

  it('DeepSeek with useBeta returns a model (beta endpoint)', () => {
    const model = upstreamModel(dsConfig, 'deepseek-v4-pro', null, undefined, true);
    expect(model).toBeDefined();
    expect((model as any).modelId).toBe('deepseek-v4-pro');
  });

  it('DeepSeek composite with useBeta returns a model', () => {
    const model = upstreamModel(dsConfig, 'deepseek/deepseek-v4-pro', null, undefined, true);
    expect(model).toBeDefined();
    expect((model as any).modelId).toBe('deepseek-v4-pro');
  });

  it('non-DeepSeek with useBeta ignores the flag', () => {
    const model = upstreamModel(dsConfig, 'qwen3.6', null, undefined, true);
    expect(model).toBeDefined();
  });
});
