import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import Fastify from 'fastify';
import type { FastifyInstance } from 'fastify';
import { registerMcpServerRoutes } from '../src/routes/mcp-servers.js';
import { McpServerService, NotFoundError, ConflictError } from '../src/services/mcp-server.service.js';
import { InstanceService } from '../src/services/instance.service.js';
import { errorHandler } from '../src/middleware/error-handler.js';
import type { IMcpServerRepository, IMcpInstanceRepository } from '../src/repositories/interfaces.js';
import type { McpOrchestrator } from '../src/services/orchestrator.js';

let app: FastifyInstance;

function mockRepo(): IMcpServerRepository {
  let lastCreated: Record<string, unknown> | null = null;
  return {
    findAll: vi.fn(async () => [
      { id: '1', name: 'slack', description: 'Slack server', transport: 'STDIO', replicas: 1 },
    ]),
    findById: vi.fn(async (id: string) => {
      if (lastCreated && (lastCreated as { id: string }).id === id) return lastCreated as never;
      return null;
    }),
    findByName: vi.fn(async () => null),
    create: vi.fn(async (data) => {
      const server = {
        id: 'new-id',
        name: data.name,
        description: data.description ?? '',
        packageName: data.packageName ?? null,
        dockerImage: null,
        transport: data.transport ?? 'STDIO',
        repositoryUrl: null,
        externalUrl: null,
        command: null,
        containerPort: null,
        replicas: data.replicas ?? 1,
        env: [],
        version: 1,
        createdAt: new Date(),
        updatedAt: new Date(),
      };
      lastCreated = server;
      return server;
    }),
    update: vi.fn(async (id, data) => {
      const server = {
        id,
        name: 'slack',
        description: (data.description as string) ?? 'Slack server',
        packageName: null,
        dockerImage: null,
        transport: 'STDIO',
        repositoryUrl: null,
        externalUrl: null,
        command: null,
        containerPort: null,
        replicas: 1,
        env: [],
        version: 2,
        createdAt: new Date(),
        updatedAt: new Date(),
      };
      lastCreated = server;
      return server;
    }),
    delete: vi.fn(async () => {}),
  };
}

afterEach(async () => {
  if (app) await app.close();
});

function stubInstanceRepo(): IMcpInstanceRepository {
  return {
    findAll: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    findByContainerId: vi.fn(async () => null),
    create: vi.fn(async (data) => ({
      id: 'inst-stub',
      serverId: data.serverId,
      containerId: null,
      status: data.status ?? 'STOPPED',
      port: null,
      metadata: {},
      version: 1,
      createdAt: new Date(),
      updatedAt: new Date(),
    })),
    updateStatus: vi.fn(async (id, status) => ({
      id,
      serverId: 'srv-1',
      containerId: null,
      status,
      port: null,
      metadata: {},
      version: 2,
      createdAt: new Date(),
      updatedAt: new Date(),
    })),
    delete: vi.fn(async () => {}),
  };
}

function stubOrchestrator(): McpOrchestrator {
  return {
    ping: vi.fn(async () => true),
    pullImage: vi.fn(async () => {}),
    createContainer: vi.fn(async () => ({ containerId: 'ctr-stub', name: 'stub', state: 'running' as const, port: 3000, createdAt: new Date() })),
    stopContainer: vi.fn(async () => {}),
    removeContainer: vi.fn(async () => {}),
    inspectContainer: vi.fn(async () => ({ containerId: 'ctr-stub', name: 'stub', state: 'running' as const, createdAt: new Date() })),
    getContainerLogs: vi.fn(async () => ({ stdout: '', stderr: '' })),
  };
}

function createApp(repo: IMcpServerRepository) {
  app = Fastify({ logger: false });
  app.setErrorHandler(errorHandler);
  const service = new McpServerService(repo);
  const instanceService = new InstanceService(stubInstanceRepo(), repo, stubOrchestrator());
  service.setInstanceService(instanceService);
  registerMcpServerRoutes(app, service, instanceService);
  return app.ready();
}

describe('MCP Server Routes', () => {
  describe('GET /api/v1/servers', () => {
    it('returns server list', async () => {
      const repo = mockRepo();
      await createApp(repo);
      const res = await app.inject({ method: 'GET', url: '/api/v1/servers' });
      expect(res.statusCode).toBe(200);
      const body = res.json<Array<{ name: string }>>();
      expect(body).toHaveLength(1);
      expect(body[0]?.name).toBe('slack');
    });
  });

  describe('GET /api/v1/servers/:id', () => {
    it('returns 404 when not found', async () => {
      const repo = mockRepo();
      await createApp(repo);
      const res = await app.inject({ method: 'GET', url: '/api/v1/servers/missing' });
      expect(res.statusCode).toBe(404);
    });

    it('returns server when found', async () => {
      const repo = mockRepo();
      vi.mocked(repo.findById).mockResolvedValue({ id: '1', name: 'slack' } as never);
      await createApp(repo);
      const res = await app.inject({ method: 'GET', url: '/api/v1/servers/1' });
      expect(res.statusCode).toBe(200);
    });
  });

  describe('POST /api/v1/servers', () => {
    it('creates a server and returns 201', async () => {
      const repo = mockRepo();
      await createApp(repo);
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/servers',
        payload: { name: 'new-server' },
      });
      expect(res.statusCode).toBe(201);
      expect(res.json<{ name: string }>().name).toBe('new-server');
    });

    it('returns 400 for invalid input', async () => {
      const repo = mockRepo();
      await createApp(repo);
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/servers',
        payload: { name: '' },
      });
      expect(res.statusCode).toBe(400);
    });

    it('returns 409 when name already exists', async () => {
      const repo = mockRepo();
      vi.mocked(repo.findByName).mockResolvedValue({ id: '1' } as never);
      await createApp(repo);
      const res = await app.inject({
        method: 'POST',
        url: '/api/v1/servers',
        payload: { name: 'existing' },
      });
      expect(res.statusCode).toBe(409);
    });
  });

  describe('PUT /api/v1/servers/:id', () => {
    it('updates a server', async () => {
      const repo = mockRepo();
      vi.mocked(repo.findById).mockResolvedValue({ id: '1', name: 'slack' } as never);
      await createApp(repo);
      const res = await app.inject({
        method: 'PUT',
        url: '/api/v1/servers/1',
        payload: { description: 'Updated' },
      });
      expect(res.statusCode).toBe(200);
    });

    it('returns 404 when not found', async () => {
      const repo = mockRepo();
      await createApp(repo);
      const res = await app.inject({
        method: 'PUT',
        url: '/api/v1/servers/missing',
        payload: { description: 'x' },
      });
      expect(res.statusCode).toBe(404);
    });
  });

  describe('DELETE /api/v1/servers/:id', () => {
    it('deletes a server and returns 204', async () => {
      const repo = mockRepo();
      vi.mocked(repo.findById).mockResolvedValue({ id: '1', name: 'slack' } as never);
      await createApp(repo);
      const res = await app.inject({ method: 'DELETE', url: '/api/v1/servers/1' });
      expect(res.statusCode).toBe(204);
    });

    it('returns 404 when not found', async () => {
      const repo = mockRepo();
      await createApp(repo);
      const res = await app.inject({ method: 'DELETE', url: '/api/v1/servers/missing' });
      expect(res.statusCode).toBe(404);
    });
  });
});
