import { describe, it, expect, vi } from 'vitest';
import { attachSkillMcpServers, parseMcpServerDeps } from '../../src/utils/mcpservers-materialiser.js';
import type { ApiClient } from '../../src/api-client.js';
import { ApiError } from '../../src/api-client.js';

interface MockClient {
  get: ReturnType<typeof vi.fn>;
  post: ReturnType<typeof vi.fn>;
  put: ReturnType<typeof vi.fn>;
  delete: ReturnType<typeof vi.fn>;
}

function makeClient(): MockClient {
  return {
    get: vi.fn(),
    post: vi.fn(async () => ({})),
    put: vi.fn(async () => ({})),
    delete: vi.fn(async () => undefined),
  };
}

function apiError(status: number, body = 'err'): ApiError {
  return new ApiError(status, body);
}

describe('mcpservers-materialiser', () => {
  describe('parseMcpServerDeps', () => {
    it('returns [] for non-arrays', () => {
      expect(parseMcpServerDeps(null)).toEqual([]);
      expect(parseMcpServerDeps('foo')).toEqual([]);
      expect(parseMcpServerDeps({})).toEqual([]);
    });

    it('keeps valid entries and drops garbage', () => {
      const out = parseMcpServerDeps([
        { name: 'good', fromTemplate: 't', project: 'p' },
        { name: '', fromTemplate: 't' },                  // empty name → drop
        { fromTemplate: 'no-name' },                       // no name → drop
        { name: 'bare' },                                  // valid, minimal
        'string',                                          // not an object → drop
      ]);
      expect(out).toEqual([
        { name: 'good', fromTemplate: 't', project: 'p' },
        { name: 'bare' },
      ]);
    });
  });

  describe('attachSkillMcpServers', () => {
    it('attaches a new server when not already present', async () => {
      const client = makeClient();
      client.get.mockImplementation(async (path: string) => {
        if (path === '/api/v1/projects') return [{ id: 'proj-1', name: 'demo' }];
        if (path === '/api/v1/projects/proj-1') return { servers: [] };
        if (path === '/api/v1/servers') return [{ name: 'my-grafana' }];
        throw new Error(`unexpected GET ${path}`);
      });

      const result = await attachSkillMcpServers(
        client as unknown as ApiClient,
        'demo',
        [{ name: 'my-grafana', fromTemplate: 'grafana' }],
      );

      expect(result.attached).toEqual(['my-grafana']);
      expect(result.alreadyAttached).toEqual([]);
      expect(result.missing).toEqual([]);
      expect(result.errors).toEqual([]);
      expect(client.post).toHaveBeenCalledWith('/api/v1/projects/proj-1/servers', { server: 'my-grafana' });
    });

    it('reports alreadyAttached without re-posting', async () => {
      const client = makeClient();
      client.get.mockImplementation(async (path: string) => {
        if (path === '/api/v1/projects') return [{ id: 'proj-1', name: 'demo' }];
        if (path === '/api/v1/projects/proj-1') return { servers: [{ server: { name: 'my-grafana' } }] };
        if (path === '/api/v1/servers') return [{ name: 'my-grafana' }];
        throw new Error(`unexpected GET ${path}`);
      });

      const result = await attachSkillMcpServers(
        client as unknown as ApiClient,
        'demo',
        [{ name: 'my-grafana' }],
      );

      expect(result.alreadyAttached).toEqual(['my-grafana']);
      expect(result.attached).toEqual([]);
      expect(client.post).not.toHaveBeenCalled();
    });

    it('warns + skips when server does not exist on mcpd', async () => {
      const client = makeClient();
      client.get.mockImplementation(async (path: string) => {
        if (path === '/api/v1/projects') return [{ id: 'proj-1', name: 'demo' }];
        if (path === '/api/v1/projects/proj-1') return { servers: [] };
        if (path === '/api/v1/servers') return [{ name: 'something-else' }];
        throw new Error(`unexpected GET ${path}`);
      });

      const warnings: string[] = [];
      const result = await attachSkillMcpServers(
        client as unknown as ApiClient,
        'demo',
        [{ name: 'my-grafana', fromTemplate: 'grafana' }],
        (m) => warnings.push(m),
      );

      expect(result.missing).toEqual(['my-grafana']);
      expect(result.attached).toEqual([]);
      expect(client.post).not.toHaveBeenCalled();
      expect(warnings.some((w) => w.includes('my-grafana') && w.includes('grafana'))).toBe(true);
    });

    it('errors-out when the project does not exist', async () => {
      const client = makeClient();
      client.get.mockImplementation(async (path: string) => {
        if (path === '/api/v1/projects') return []; // no projects
        throw new Error(`unexpected GET ${path}`);
      });

      const result = await attachSkillMcpServers(
        client as unknown as ApiClient,
        'no-such-project',
        [{ name: 'my-grafana' }],
      );

      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]?.error).toContain('Project');
      expect(client.post).not.toHaveBeenCalled();
    });

    it('treats 409 from POST as alreadyAttached (idempotent server-side)', async () => {
      const client = makeClient();
      client.get.mockImplementation(async (path: string) => {
        if (path === '/api/v1/projects') return [{ id: 'proj-1', name: 'demo' }];
        // attachments listing fails — fall back to attempting + handling 409
        if (path === '/api/v1/projects/proj-1') throw apiError(500, 'flake');
        if (path === '/api/v1/servers') return [{ name: 'my-grafana' }];
        throw new Error(`unexpected GET ${path}`);
      });
      client.post.mockRejectedValueOnce(apiError(409, 'already attached'));

      const result = await attachSkillMcpServers(
        client as unknown as ApiClient,
        'demo',
        [{ name: 'my-grafana' }],
      );

      expect(result.alreadyAttached).toEqual(['my-grafana']);
      expect(result.errors).toEqual([]);
    });

    it('treats 404 from POST as missing (server vanished mid-sync)', async () => {
      const client = makeClient();
      client.get.mockImplementation(async (path: string) => {
        if (path === '/api/v1/projects') return [{ id: 'proj-1', name: 'demo' }];
        if (path === '/api/v1/projects/proj-1') return { servers: [] };
        if (path === '/api/v1/servers') return [{ name: 'my-grafana' }]; // existed when we listed
        throw new Error(`unexpected GET ${path}`);
      });
      // …but vanished by the time we POSTed.
      client.post.mockRejectedValueOnce(apiError(404, 'gone'));

      const result = await attachSkillMcpServers(
        client as unknown as ApiClient,
        'demo',
        [{ name: 'my-grafana' }],
      );

      expect(result.missing).toEqual(['my-grafana']);
      expect(result.errors).toEqual([]);
    });

    it('skips deps that target a different project', async () => {
      const client = makeClient();
      client.get.mockImplementation(async (path: string) => {
        if (path === '/api/v1/projects') return [{ id: 'proj-1', name: 'demo' }];
        if (path === '/api/v1/projects/proj-1') return { servers: [] };
        if (path === '/api/v1/servers') return [{ name: 'my-grafana' }];
        throw new Error(`unexpected GET ${path}`);
      });

      const result = await attachSkillMcpServers(
        client as unknown as ApiClient,
        'demo',
        [{ name: 'my-grafana', project: 'other-project' }],
      );

      expect(result.attached).toEqual([]);
      expect(result.missing).toEqual([]);
      expect(client.post).not.toHaveBeenCalled();
    });

    it('continues past per-server errors', async () => {
      const client = makeClient();
      client.get.mockImplementation(async (path: string) => {
        if (path === '/api/v1/projects') return [{ id: 'proj-1', name: 'demo' }];
        if (path === '/api/v1/projects/proj-1') return { servers: [] };
        if (path === '/api/v1/servers') return [{ name: 'a' }, { name: 'b' }];
        throw new Error(`unexpected GET ${path}`);
      });
      client.post.mockImplementation(async (path: string, body) => {
        if ((body as { server: string }).server === 'a') throw apiError(500, 'boom');
        return {};
      });

      const result = await attachSkillMcpServers(
        client as unknown as ApiClient,
        'demo',
        [{ name: 'a' }, { name: 'b' }],
      );

      expect(result.errors).toHaveLength(1);
      expect(result.errors[0]?.server).toBe('a');
      expect(result.attached).toEqual(['b']);
    });

    it('returns empty on empty deps without making any calls', async () => {
      const client = makeClient();
      const result = await attachSkillMcpServers(client as unknown as ApiClient, 'demo', []);
      expect(result).toEqual({ attached: [], alreadyAttached: [], missing: [], errors: [] });
      expect(client.get).not.toHaveBeenCalled();
      expect(client.post).not.toHaveBeenCalled();
    });
  });
});
