import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LinkResolver } from '../src/services/link-resolver.js';
import type { McpdClient } from '../src/http/mcpd-client.js';

function mockClient(): McpdClient {
  return {
    get: vi.fn(),
    post: vi.fn(),
    put: vi.fn(),
    delete: vi.fn(),
    forward: vi.fn(),
    withHeaders: vi.fn(),
  } as unknown as McpdClient;
}

describe('LinkResolver', () => {
  let client: McpdClient;
  let resolver: LinkResolver;

  beforeEach(() => {
    client = mockClient();
    resolver = new LinkResolver(client, 1000); // 1s TTL for tests
  });

  // ── parseLink ──

  describe('parseLink', () => {
    it('parses valid link target', () => {
      const result = resolver.parseLink('my-project/docmost-mcp:docmost://pages/abc');
      expect(result).toEqual({
        project: 'my-project',
        server: 'docmost-mcp',
        uri: 'docmost://pages/abc',
      });
    });

    it('parses link with complex URI', () => {
      const result = resolver.parseLink('proj/srv:file:///path/to/resource');
      expect(result).toEqual({
        project: 'proj',
        server: 'srv',
        uri: 'file:///path/to/resource',
      });
    });

    it('throws on missing project separator', () => {
      expect(() => resolver.parseLink('noslash')).toThrow('missing project');
    });

    it('throws on missing server:uri separator', () => {
      expect(() => resolver.parseLink('proj/nocolon')).toThrow('missing server:uri');
    });

    it('throws on empty uri', () => {
      expect(() => resolver.parseLink('proj/srv:')).toThrow('empty uri');
    });

    it('throws when project is empty', () => {
      expect(() => resolver.parseLink('/srv:uri')).toThrow('missing project');
    });

    it('throws when server is empty', () => {
      expect(() => resolver.parseLink('proj/:uri')).toThrow('missing server:uri');
    });
  });

  // ── resolve ──

  describe('resolve', () => {
    it('fetches resource content successfully', async () => {
      vi.mocked(client.get).mockResolvedValue([
        { id: 'srv-id-1', name: 'docmost-mcp' },
      ]);
      vi.mocked(client.post).mockResolvedValue({
        result: { contents: [{ text: 'Hello from docmost', uri: 'docmost://pages/abc' }] },
      });

      const result = await resolver.resolve('my-project/docmost-mcp:docmost://pages/abc');

      expect(result).toEqual({ content: 'Hello from docmost', status: 'alive' });
      expect(client.get).toHaveBeenCalledWith('/api/v1/projects/my-project/servers');
      expect(client.post).toHaveBeenCalledWith('/api/v1/mcp/proxy', {
        serverId: 'srv-id-1',
        method: 'resources/read',
        params: { uri: 'docmost://pages/abc' },
      });
    });

    it('returns dead status when server not found in project', async () => {
      vi.mocked(client.get).mockResolvedValue([
        { id: 'srv-other', name: 'other-server' },
      ]);

      const result = await resolver.resolve('proj/missing-srv:some://uri');

      expect(result.status).toBe('dead');
      expect(result.content).toBeNull();
      expect(result.error).toContain("Server 'missing-srv' not found");
    });

    it('returns dead status when MCP proxy returns error', async () => {
      vi.mocked(client.get).mockResolvedValue([{ id: 'srv-1', name: 'srv' }]);
      vi.mocked(client.post).mockResolvedValue({
        error: { code: -32601, message: 'Method not found' },
      });

      const result = await resolver.resolve('proj/srv:some://uri');

      expect(result.status).toBe('dead');
      expect(result.error).toContain('Method not found');
    });

    it('returns dead status when no content returned', async () => {
      vi.mocked(client.get).mockResolvedValue([{ id: 'srv-1', name: 'srv' }]);
      vi.mocked(client.post).mockResolvedValue({
        result: { contents: [] },
      });

      const result = await resolver.resolve('proj/srv:some://uri');

      expect(result.status).toBe('dead');
      expect(result.error).toContain('No content returned');
    });

    it('returns dead status on network error', async () => {
      vi.mocked(client.get).mockRejectedValue(new Error('Connection refused'));

      const result = await resolver.resolve('proj/srv:some://uri');

      expect(result.status).toBe('dead');
      expect(result.error).toContain('Connection refused');
    });

    it('concatenates multiple content entries', async () => {
      vi.mocked(client.get).mockResolvedValue([{ id: 'srv-1', name: 'srv' }]);
      vi.mocked(client.post).mockResolvedValue({
        result: {
          contents: [
            { text: 'Part 1', uri: 'uri1' },
            { text: 'Part 2', uri: 'uri2' },
          ],
        },
      });

      const result = await resolver.resolve('proj/srv:some://uri');

      expect(result.content).toBe('Part 1\nPart 2');
      expect(result.status).toBe('alive');
    });

    it('logs dead link to console.error', async () => {
      vi.mocked(client.get).mockRejectedValue(new Error('fail'));
      const spy = vi.spyOn(console, 'error').mockImplementation(() => {});

      await resolver.resolve('proj/srv:some://uri');

      expect(spy).toHaveBeenCalledWith(expect.stringContaining('[link-resolver] Dead link'));
      spy.mockRestore();
    });
  });

  // ── caching ──

  describe('caching', () => {
    it('returns cached result on second call', async () => {
      vi.mocked(client.get).mockResolvedValue([{ id: 'srv-1', name: 'srv' }]);
      vi.mocked(client.post).mockResolvedValue({
        result: { contents: [{ text: 'cached content' }] },
      });

      const first = await resolver.resolve('proj/srv:some://uri');
      const second = await resolver.resolve('proj/srv:some://uri');

      expect(first).toEqual(second);
      // Only one HTTP call — second was cached
      expect(client.get).toHaveBeenCalledTimes(1);
    });

    it('refetches after cache expires', async () => {
      vi.mocked(client.get).mockResolvedValue([{ id: 'srv-1', name: 'srv' }]);
      vi.mocked(client.post).mockResolvedValue({
        result: { contents: [{ text: 'content' }] },
      });

      await resolver.resolve('proj/srv:some://uri');

      // Advance time past TTL
      vi.useFakeTimers();
      vi.advanceTimersByTime(1500);

      await resolver.resolve('proj/srv:some://uri');

      expect(client.get).toHaveBeenCalledTimes(2);
      vi.useRealTimers();
    });

    it('clearCache removes all entries', async () => {
      vi.mocked(client.get).mockResolvedValue([{ id: 'srv-1', name: 'srv' }]);
      vi.mocked(client.post).mockResolvedValue({
        result: { contents: [{ text: 'content' }] },
      });

      await resolver.resolve('proj/srv:some://uri');
      resolver.clearCache();
      await resolver.resolve('proj/srv:some://uri');

      expect(client.get).toHaveBeenCalledTimes(2);
    });
  });

  // ── checkHealth ──

  describe('checkHealth', () => {
    it('returns cached status if available', async () => {
      vi.mocked(client.get).mockResolvedValue([{ id: 'srv-1', name: 'srv' }]);
      vi.mocked(client.post).mockResolvedValue({
        result: { contents: [{ text: 'content' }] },
      });

      await resolver.resolve('proj/srv:some://uri');
      const health = await resolver.checkHealth('proj/srv:some://uri');

      expect(health).toBe('alive');
    });

    it('returns unknown if not cached', async () => {
      const health = await resolver.checkHealth('proj/srv:some://uri');
      expect(health).toBe('unknown');
    });

    it('returns dead from cached dead link', async () => {
      vi.mocked(client.get).mockRejectedValue(new Error('fail'));
      vi.spyOn(console, 'error').mockImplementation(() => {});

      await resolver.resolve('proj/srv:some://uri');
      const health = await resolver.checkHealth('proj/srv:some://uri');

      expect(health).toBe('dead');
    });
  });
});
