import { describe, it, expect, vi, beforeEach } from 'vitest';
import Fastify from 'fastify';
import type { FastifyInstance } from 'fastify';
import { registerProjectMcpEndpoint } from '../src/http/project-mcp-endpoint.js';

// Mock discovery module — we don't want real HTTP calls
vi.mock('../src/discovery.js', () => ({
  refreshProjectUpstreams: vi.fn(async () => ['mock-server']),
  fetchProjectLlmConfig: vi.fn(async () => ({})),
}));

// Mock config module — don't read real config files
vi.mock('../src/http/config.js', async () => {
  const actual = await vi.importActual<typeof import('../src/http/config.js')>('../src/http/config.js');
  return {
    ...actual,
    loadProjectLlmOverride: vi.fn(() => undefined),
  };
});

import { refreshProjectUpstreams } from '../src/discovery.js';

function mockMcpdClient() {
  const client: Record<string, unknown> = {
    baseUrl: 'http://test:3100',
    token: 'test-token',
    get: vi.fn(async () => []),
    post: vi.fn(async () => ({})),
    put: vi.fn(),
    delete: vi.fn(),
    forward: vi.fn(async () => ({ status: 200, body: [] })),
    withHeaders: vi.fn(),
  };
  // withHeaders returns a new client-like object (returns self for simplicity)
  (client.withHeaders as ReturnType<typeof vi.fn>).mockReturnValue(client);
  return client;
}

describe('registerProjectMcpEndpoint', () => {
  let app: FastifyInstance;

  beforeEach(async () => {
    vi.clearAllMocks();
    app = Fastify();
    registerProjectMcpEndpoint(app, mockMcpdClient() as any);
    await app.ready();
  });

  it('registers POST /projects/:projectName/mcp route', async () => {
    // The endpoint should exist and attempt to handle MCP protocol
    const res = await app.inject({
      method: 'POST',
      url: '/projects/smart-home/mcp',
      payload: { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
      headers: { 'content-type': 'application/json' },
    });

    // The StreamableHTTPServerTransport hijacks the response,
    // so we may get a 200 or the transport handles it directly
    expect(res.statusCode).not.toBe(404);
  });

  it('calls refreshProjectUpstreams with project name', async () => {
    await app.inject({
      method: 'POST',
      url: '/projects/smart-home/mcp',
      payload: { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
      headers: { 'content-type': 'application/json' },
    });

    expect(refreshProjectUpstreams).toHaveBeenCalledWith(
      expect.any(Object), // McpRouter instance
      expect.any(Object), // McpdClient
      'smart-home',
      undefined, // no auth token
    );
  });

  it('forwards auth token from Authorization header', async () => {
    await app.inject({
      method: 'POST',
      url: '/projects/secure-project/mcp',
      payload: { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
      headers: {
        'content-type': 'application/json',
        'authorization': 'Bearer my-token-123',
      },
    });

    expect(refreshProjectUpstreams).toHaveBeenCalledWith(
      expect.any(Object),
      expect.any(Object),
      'secure-project',
      'my-token-123',
    );
  });

  it('returns 502 when project discovery fails', async () => {
    vi.mocked(refreshProjectUpstreams).mockRejectedValueOnce(new Error('Forbidden'));

    const res = await app.inject({
      method: 'POST',
      url: '/projects/bad-project/mcp',
      payload: { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
      headers: { 'content-type': 'application/json' },
    });

    expect(res.statusCode).toBe(502);
    expect(res.json().error).toContain('Failed to load project');
  });

  it('returns 404 for unknown session ID', async () => {
    const res = await app.inject({
      method: 'POST',
      url: '/projects/smart-home/mcp',
      payload: { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} },
      headers: {
        'content-type': 'application/json',
        'mcp-session-id': 'nonexistent-session',
      },
    });

    expect(res.statusCode).toBe(404);
  });

  it('returns 400 for GET without session', async () => {
    const res = await app.inject({
      method: 'GET',
      url: '/projects/smart-home/mcp',
    });

    expect(res.statusCode).toBe(400);
    expect(res.json().error).toContain('session');
  });

  it('returns 400 for DELETE without session', async () => {
    const res = await app.inject({
      method: 'DELETE',
      url: '/projects/smart-home/mcp',
    });

    expect(res.statusCode).toBe(400);
    expect(res.json().error).toContain('session');
  });

  it('caches project router across requests', async () => {
    // Two requests to the same project should reuse the router
    await app.inject({
      method: 'POST',
      url: '/projects/smart-home/mcp',
      payload: { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
      headers: { 'content-type': 'application/json' },
    });

    await app.inject({
      method: 'POST',
      url: '/projects/smart-home/mcp',
      payload: { jsonrpc: '2.0', id: 2, method: 'initialize', params: {} },
      headers: { 'content-type': 'application/json' },
    });

    // refreshProjectUpstreams should only be called once (cached)
    expect(refreshProjectUpstreams).toHaveBeenCalledTimes(1);
  });

  it('creates separate routers for different projects', async () => {
    await app.inject({
      method: 'POST',
      url: '/projects/project-a/mcp',
      payload: { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} },
      headers: { 'content-type': 'application/json' },
    });

    await app.inject({
      method: 'POST',
      url: '/projects/project-b/mcp',
      payload: { jsonrpc: '2.0', id: 2, method: 'initialize', params: {} },
      headers: { 'content-type': 'application/json' },
    });

    // Two different projects should trigger two refreshes
    expect(refreshProjectUpstreams).toHaveBeenCalledTimes(2);
    expect(refreshProjectUpstreams).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'project-a', undefined);
    expect(refreshProjectUpstreams).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), 'project-b', undefined);
  });
});
