import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ProjectService } from '../src/services/project.service.js';
import { NotFoundError, ConflictError } from '../src/services/mcp-server.service.js';
import type { IProjectRepository, ProjectWithRelations } from '../src/repositories/project.repository.js';
import type { IMcpServerRepository } from '../src/repositories/interfaces.js';
import type { McpServer } from '@prisma/client';

function makeProject(overrides: Partial<ProjectWithRelations> = {}): ProjectWithRelations {
  return {
    id: 'proj-1',
    name: 'test-project',
    description: '',
    ownerId: 'user-1',
    proxyModel: '',
    gated: true,
    llmProvider: null,
    llmModel: null,
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    servers: [],
    ...overrides,
  };
}

function makeServer(overrides: Partial<McpServer> = {}): McpServer {
  return {
    id: 'srv-1',
    name: 'test-server',
    description: '',
    packageName: '@mcp/test',
    dockerImage: null,
    transport: 'STDIO',
    repositoryUrl: null,
    externalUrl: null,
    command: null,
    containerPort: null,
    replicas: 1,
    env: [],
    healthCheck: null,
    version: 1,
    createdAt: new Date(),
    updatedAt: new Date(),
    templateName: null,
    templateVersion: null,
    ...overrides,
  };
}

function mockProjectRepo(): IProjectRepository {
  return {
    findAll: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    findByName: vi.fn(async () => null),
    create: vi.fn(async (data) => makeProject({
      name: data.name,
      description: data.description,
      ownerId: data.ownerId,
      llmProvider: data.llmProvider ?? null,
      llmModel: data.llmModel ?? null,
    })),
    update: vi.fn(async (_id, data) => makeProject({ ...data as Partial<ProjectWithRelations> })),
    delete: vi.fn(async () => {}),
    setServers: vi.fn(async () => {}),
    addServer: vi.fn(async () => {}),
    removeServer: vi.fn(async () => {}),
  };
}

function mockServerRepo(): IMcpServerRepository {
  return {
    findAll: vi.fn(async () => []),
    findById: vi.fn(async () => null),
    findByName: vi.fn(async () => null),
    create: vi.fn(async () => makeServer()),
    update: vi.fn(async () => makeServer()),
    delete: vi.fn(async () => {}),
  };
}

describe('ProjectService', () => {
  let projectRepo: ReturnType<typeof mockProjectRepo>;
  let serverRepo: ReturnType<typeof mockServerRepo>;
  let service: ProjectService;

  beforeEach(() => {
    projectRepo = mockProjectRepo();
    serverRepo = mockServerRepo();
    service = new ProjectService(projectRepo, serverRepo);
  });

  describe('create', () => {
    it('creates a basic project', async () => {
      // After create, getById is called to re-fetch with relations
      const created = makeProject({ name: 'my-project', ownerId: 'user-1' });
      vi.mocked(projectRepo.findById).mockResolvedValue(created);

      const result = await service.create({ name: 'my-project' }, 'user-1');
      expect(result.name).toBe('my-project');
      expect(result.ownerId).toBe('user-1');
      expect(projectRepo.create).toHaveBeenCalled();
    });

    it('throws ConflictError when name exists', async () => {
      vi.mocked(projectRepo.findByName).mockResolvedValue(makeProject());
      await expect(service.create({ name: 'taken' }, 'u1')).rejects.toThrow(ConflictError);
    });

    it('validates input', async () => {
      await expect(service.create({ name: '' }, 'u1')).rejects.toThrow();
    });

    it('creates project with servers (resolves names)', async () => {
      const srv1 = makeServer({ id: 'srv-1', name: 'github' });
      const srv2 = makeServer({ id: 'srv-2', name: 'slack' });
      vi.mocked(serverRepo.findByName).mockImplementation(async (name) => {
        if (name === 'github') return srv1;
        if (name === 'slack') return srv2;
        return null;
      });

      const created = makeProject({ id: 'proj-new' });
      vi.mocked(projectRepo.create).mockResolvedValue(created);
      vi.mocked(projectRepo.findById).mockResolvedValue(makeProject({
        id: 'proj-new',
        servers: [
          { id: 'ps-1', server: { id: 'srv-1', name: 'github' } },
          { id: 'ps-2', server: { id: 'srv-2', name: 'slack' } },
        ],
      }));

      const result = await service.create({ name: 'my-project', servers: ['github', 'slack'] }, 'user-1');
      expect(projectRepo.setServers).toHaveBeenCalledWith('proj-new', ['srv-1', 'srv-2']);
      expect(result.servers).toHaveLength(2);
    });

    it('throws NotFoundError when server name resolution fails', async () => {
      vi.mocked(serverRepo.findByName).mockResolvedValue(null);

      await expect(
        service.create({ name: 'my-project', servers: ['nonexistent'] }, 'user-1'),
      ).rejects.toThrow(NotFoundError);
    });

  });

  describe('getById', () => {
    it('throws NotFoundError when not found', async () => {
      await expect(service.getById('missing')).rejects.toThrow(NotFoundError);
    });

    it('returns project when found', async () => {
      const proj = makeProject({ id: 'found' });
      vi.mocked(projectRepo.findById).mockResolvedValue(proj);
      const result = await service.getById('found');
      expect(result.id).toBe('found');
    });
  });

  describe('resolveAndGet', () => {
    it('finds by ID first', async () => {
      const proj = makeProject({ id: 'proj-id' });
      vi.mocked(projectRepo.findById).mockResolvedValue(proj);
      const result = await service.resolveAndGet('proj-id');
      expect(result.id).toBe('proj-id');
    });

    it('falls back to name when ID not found', async () => {
      vi.mocked(projectRepo.findById).mockResolvedValue(null);
      const proj = makeProject({ name: 'my-name' });
      vi.mocked(projectRepo.findByName).mockResolvedValue(proj);
      const result = await service.resolveAndGet('my-name');
      expect(result.name).toBe('my-name');
    });

    it('throws NotFoundError when neither ID nor name found', async () => {
      await expect(service.resolveAndGet('nothing')).rejects.toThrow(NotFoundError);
    });
  });

  describe('update', () => {
    it('updates servers (full replacement)', async () => {
      const existing = makeProject({ id: 'proj-1' });
      vi.mocked(projectRepo.findById).mockResolvedValue(existing);

      const srv = makeServer({ id: 'srv-new', name: 'new-srv' });
      vi.mocked(serverRepo.findByName).mockResolvedValue(srv);

      await service.update('proj-1', { servers: ['new-srv'] });
      expect(projectRepo.setServers).toHaveBeenCalledWith('proj-1', ['srv-new']);
    });

    it('updates llmProvider', async () => {
      const existing = makeProject({ id: 'proj-1' });
      vi.mocked(projectRepo.findById).mockResolvedValue(existing);

      await service.update('proj-1', { llmProvider: 'anthropic' });
      expect(projectRepo.update).toHaveBeenCalledWith('proj-1', {
        llmProvider: 'anthropic',
      });
    });
  });

  describe('delete', () => {
    it('deletes project', async () => {
      vi.mocked(projectRepo.findById).mockResolvedValue(makeProject({ id: 'p1' }));
      await service.delete('p1');
      expect(projectRepo.delete).toHaveBeenCalledWith('p1');
    });

    it('throws NotFoundError when project does not exist', async () => {
      await expect(service.delete('missing')).rejects.toThrow(NotFoundError);
    });
  });

  describe('addServer', () => {
    it('attaches a server by name', async () => {
      const project = makeProject({ id: 'proj-1' });
      const srv = makeServer({ id: 'srv-1', name: 'my-ha' });
      vi.mocked(projectRepo.findById).mockResolvedValue(project);
      vi.mocked(serverRepo.findByName).mockResolvedValue(srv);

      await service.addServer('proj-1', 'my-ha');
      expect(projectRepo.addServer).toHaveBeenCalledWith('proj-1', 'srv-1');
    });

    it('throws NotFoundError when project not found', async () => {
      await expect(service.addServer('missing', 'my-ha')).rejects.toThrow(NotFoundError);
    });

    it('throws NotFoundError when server not found', async () => {
      vi.mocked(projectRepo.findById).mockResolvedValue(makeProject({ id: 'proj-1' }));
      vi.mocked(serverRepo.findByName).mockResolvedValue(null);

      await expect(service.addServer('proj-1', 'nonexistent')).rejects.toThrow(NotFoundError);
    });
  });

  describe('removeServer', () => {
    it('detaches a server by name', async () => {
      const project = makeProject({ id: 'proj-1' });
      const srv = makeServer({ id: 'srv-1', name: 'my-ha' });
      vi.mocked(projectRepo.findById).mockResolvedValue(project);
      vi.mocked(serverRepo.findByName).mockResolvedValue(srv);

      await service.removeServer('proj-1', 'my-ha');
      expect(projectRepo.removeServer).toHaveBeenCalledWith('proj-1', 'srv-1');
    });

    it('throws NotFoundError when project not found', async () => {
      await expect(service.removeServer('missing', 'my-ha')).rejects.toThrow(NotFoundError);
    });

    it('throws NotFoundError when server not found', async () => {
      vi.mocked(projectRepo.findById).mockResolvedValue(makeProject({ id: 'proj-1' }));
      vi.mocked(serverRepo.findByName).mockResolvedValue(null);

      await expect(service.removeServer('proj-1', 'nonexistent')).rejects.toThrow(NotFoundError);
    });
  });

  describe('generateMcpConfig', () => {
    it('generates single mcplocal proxy entry', async () => {
      const project = makeProject({
        id: 'proj-1',
        name: 'my-proj',
        servers: [{ id: 'ps-1', server: { id: 'srv-1', name: 'github' } }],
      });

      vi.mocked(projectRepo.findById).mockResolvedValue(project);

      const config = await service.generateMcpConfig('proj-1');
      expect(Object.keys(config.mcpServers)).toHaveLength(1);
      expect(config.mcpServers['my-proj']?.url).toBe('http://localhost:3100/api/v1/mcp/proxy/project/my-proj');
    });

    it('resolves by name for mcp-config', async () => {
      const project = makeProject({
        id: 'proj-1',
        name: 'my-proj',
        servers: [],
      });

      vi.mocked(projectRepo.findById).mockResolvedValue(null);
      vi.mocked(projectRepo.findByName).mockResolvedValue(project);

      const config = await service.generateMcpConfig('my-proj');
      expect(Object.keys(config.mcpServers)).toHaveLength(1);
      expect(config.mcpServers['my-proj']?.url).toBe('http://localhost:3100/api/v1/mcp/proxy/project/my-proj');
    });
  });
});
