import { describe, it, expect, vi, afterEach } from 'vitest';
import { ResponsePaginator, DEFAULT_PAGINATION_CONFIG } from '../src/llm/pagination.js';
import type { ProviderRegistry } from '../src/providers/registry.js';
import type { LlmProvider } from '../src/providers/types.js';

function makeProvider(response: string): ProviderRegistry {
  const provider: LlmProvider = {
    name: 'test',
    isAvailable: () => true,
    complete: vi.fn().mockResolvedValue({ content: response }),
  };
  return {
    getActive: () => provider,
    getProvider: () => provider,
    register: vi.fn(),
    setActive: vi.fn(),
    listProviders: () => [{ name: 'test', available: true, active: true }],
  } as unknown as ProviderRegistry;
}

function makeLargeString(size: number, pattern = 'x'): string {
  return pattern.repeat(size);
}

function makeLargeStringWithNewlines(size: number, lineLen = 100): string {
  const lines: string[] = [];
  let total = 0;
  let lineNum = 0;
  while (total < size) {
    const line = `line-${String(lineNum).padStart(5, '0')} ${'x'.repeat(lineLen - 15)}`;
    lines.push(line);
    total += line.length + 1; // +1 for newline
    lineNum++;
  }
  return lines.join('\n');
}

describe('ResponsePaginator', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  // --- shouldPaginate ---

  describe('shouldPaginate', () => {
    it('returns false for strings below threshold', () => {
      const paginator = new ResponsePaginator(null);
      expect(paginator.shouldPaginate('short string')).toBe(false);
    });

    it('returns false for strings just below threshold', () => {
      const paginator = new ResponsePaginator(null);
      const str = makeLargeString(DEFAULT_PAGINATION_CONFIG.sizeThreshold - 1);
      expect(paginator.shouldPaginate(str)).toBe(false);
    });

    it('returns true for strings at threshold', () => {
      const paginator = new ResponsePaginator(null);
      const str = makeLargeString(DEFAULT_PAGINATION_CONFIG.sizeThreshold);
      expect(paginator.shouldPaginate(str)).toBe(true);
    });

    it('returns true for strings above threshold', () => {
      const paginator = new ResponsePaginator(null);
      const str = makeLargeString(DEFAULT_PAGINATION_CONFIG.sizeThreshold + 1000);
      expect(paginator.shouldPaginate(str)).toBe(true);
    });

    it('respects custom threshold', () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100 });
      expect(paginator.shouldPaginate('x'.repeat(99))).toBe(false);
      expect(paginator.shouldPaginate('x'.repeat(100))).toBe(true);
    });
  });

  // --- paginate (no LLM) ---

  describe('paginate without LLM', () => {
    it('returns null for small responses', async () => {
      const paginator = new ResponsePaginator(null);
      const result = await paginator.paginate('test/tool', 'small response');
      expect(result).toBeNull();
    });

    it('paginates large responses with simple index', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      const raw = makeLargeStringWithNewlines(200);
      const result = await paginator.paginate('test/tool', raw);

      expect(result).not.toBeNull();
      expect(result!.content).toHaveLength(1);
      expect(result!.content[0]!.type).toBe('text');

      const text = result!.content[0]!.text;
      expect(text).toContain('too large to return directly');
      expect(text).toContain('_resultId');
      expect(text).toContain('_page');
      expect(text).not.toContain('AI-generated summaries');
    });

    it('includes correct page count in index', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'x'.repeat(200);
      const result = await paginator.paginate('test/tool', raw);

      expect(result).not.toBeNull();
      const text = result!.content[0]!.text;
      // 200 chars / 50 per page = 4 pages
      expect(text).toContain('4 pages');
      expect(text).toContain('Page 1:');
      expect(text).toContain('Page 4:');
    });

    it('caches the result for later page retrieval', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'x'.repeat(200);
      await paginator.paginate('test/tool', raw);

      expect(paginator.cacheSize).toBe(1);
    });

    it('includes page instructions with _resultId and _page', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'x'.repeat(200);
      const result = await paginator.paginate('test/tool', raw);

      const text = result!.content[0]!.text;
      expect(text).toContain('"_resultId"');
      expect(text).toContain('"_page"');
      expect(text).toContain('"all"');
    });
  });

  // --- paginate (with LLM) ---

  describe('paginate with LLM', () => {
    it('generates smart index when provider available', async () => {
      const summaries = JSON.stringify([
        { page: 1, summary: 'Configuration nodes and global settings' },
        { page: 2, summary: 'HTTP request nodes and API integrations' },
      ]);
      const registry = makeProvider(summaries);
      const paginator = new ResponsePaginator(registry, { sizeThreshold: 100, pageSize: 60 });
      const raw = makeLargeStringWithNewlines(150);
      const result = await paginator.paginate('node-red/get_flows', raw);

      expect(result).not.toBeNull();
      const text = result!.content[0]!.text;
      expect(text).toContain('AI-generated summaries');
      expect(text).toContain('Configuration nodes and global settings');
      expect(text).toContain('HTTP request nodes and API integrations');
    });

    it('strips markdown code fences from LLM JSON response', async () => {
      const summaries = [
        { page: 1, summary: 'Config section' },
        { page: 2, summary: 'Data section' },
      ];
      // Gemini often wraps JSON in ```json ... ``` fences
      const fenced = '```json\n' + JSON.stringify(summaries) + '\n```';
      const registry = makeProvider(fenced);
      const paginator = new ResponsePaginator(registry, { sizeThreshold: 100, pageSize: 60 });
      const raw = makeLargeStringWithNewlines(150);
      const result = await paginator.paginate('test/tool', raw);

      expect(result).not.toBeNull();
      const text = result!.content[0]!.text;
      expect(text).toContain('AI-generated summaries');
      expect(text).toContain('Config section');
      expect(text).toContain('Data section');
    });

    it('falls back to simple index on LLM failure', async () => {
      const provider: LlmProvider = {
        name: 'test',
        isAvailable: () => true,
        complete: vi.fn().mockRejectedValue(new Error('LLM unavailable')),
      };
      const registry = {
        getActive: () => provider,
        getProvider: () => provider,
        register: vi.fn(),
        setActive: vi.fn(),
        listProviders: () => [{ name: 'test', available: true, active: true }],
      } as unknown as ProviderRegistry;

      const paginator = new ResponsePaginator(registry, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'x'.repeat(200);
      const result = await paginator.paginate('test/tool', raw);

      expect(result).not.toBeNull();
      const text = result!.content[0]!.text;
      // Should NOT contain AI-generated label
      expect(text).not.toContain('AI-generated summaries');
      expect(text).toContain('Page 1:');
    });

    it('sends page previews to LLM, not full content', async () => {
      const completeFn = vi.fn().mockResolvedValue({
        content: JSON.stringify([
          { page: 1, summary: 'test' },
          { page: 2, summary: 'test2' },
          { page: 3, summary: 'test3' },
        ]),
      });
      const provider: LlmProvider = {
        name: 'test',
        isAvailable: () => true,
        complete: completeFn,
      };
      const registry = {
        getActive: () => provider,
        getProvider: () => provider,
        register: vi.fn(),
        setActive: vi.fn(),
        listProviders: () => [{ name: 'test', available: true, active: true }],
      } as unknown as ProviderRegistry;

      // Use a large enough string (3000 chars, pages of 1000) so previews (500 per page) are smaller than raw
      const paginator = new ResponsePaginator(registry, { sizeThreshold: 2000, pageSize: 1000 });
      const raw = makeLargeStringWithNewlines(3000);
      await paginator.paginate('test/tool', raw);

      expect(completeFn).toHaveBeenCalledOnce();
      const call = completeFn.mock.calls[0]![0]!;
      const userMsg = call.messages.find((m: { role: string }) => m.role === 'user');
      // Should contain page preview markers
      expect(userMsg.content).toContain('Page 1');
      // The LLM prompt should be significantly smaller than the full content
      // (each page sends ~500 chars preview, not full 1000 chars)
      expect(userMsg.content.length).toBeLessThan(raw.length);
    });

    it('falls back to simple when no active provider', async () => {
      const registry = {
        getActive: () => null,
        getProvider: () => null,
        register: vi.fn(),
        setActive: vi.fn(),
        listProviders: () => [],
      } as unknown as ProviderRegistry;

      const paginator = new ResponsePaginator(registry, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'x'.repeat(200);
      const result = await paginator.paginate('test/tool', raw);

      expect(result).not.toBeNull();
      const text = result!.content[0]!.text;
      expect(text).not.toContain('AI-generated summaries');
    });

    it('passes modelOverride to provider.complete()', async () => {
      const completeFn = vi.fn().mockResolvedValue({
        content: JSON.stringify([{ page: 1, summary: 'test' }, { page: 2, summary: 'test2' }]),
      });
      const provider: LlmProvider = {
        name: 'test',
        isAvailable: () => true,
        complete: completeFn,
      };
      const registry = {
        getActive: () => provider,
        getProvider: () => provider,
        register: vi.fn(),
        setActive: vi.fn(),
        listProviders: () => [{ name: 'test', available: true, active: true }],
      } as unknown as ProviderRegistry;

      const paginator = new ResponsePaginator(registry, { sizeThreshold: 100, pageSize: 60 }, 'gemini-2.5-pro');
      const raw = makeLargeStringWithNewlines(150);
      await paginator.paginate('test/tool', raw);

      expect(completeFn).toHaveBeenCalledOnce();
      const call = completeFn.mock.calls[0]![0]!;
      expect(call.model).toBe('gemini-2.5-pro');
    });

    it('omits model when no modelOverride set', async () => {
      const completeFn = vi.fn().mockResolvedValue({
        content: JSON.stringify([{ page: 1, summary: 'test' }, { page: 2, summary: 'test2' }]),
      });
      const provider: LlmProvider = {
        name: 'test',
        isAvailable: () => true,
        complete: completeFn,
      };
      const registry = {
        getActive: () => provider,
        getProvider: () => provider,
        register: vi.fn(),
        setActive: vi.fn(),
        listProviders: () => [{ name: 'test', available: true, active: true }],
      } as unknown as ProviderRegistry;

      const paginator = new ResponsePaginator(registry, { sizeThreshold: 100, pageSize: 60 });
      const raw = makeLargeStringWithNewlines(150);
      await paginator.paginate('test/tool', raw);

      expect(completeFn).toHaveBeenCalledOnce();
      const call = completeFn.mock.calls[0]![0]!;
      expect(call.model).toBeUndefined();
    });
  });

  // --- getPage ---

  describe('getPage', () => {
    it('returns specific page content', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'AAAA'.repeat(25) + 'BBBB'.repeat(25); // 200 chars total
      await paginator.paginate('test/tool', raw);

      // Extract resultId from cache (there should be exactly 1 entry)
      expect(paginator.cacheSize).toBe(1);

      // We need the resultId — get it from the index response
      const indexResult = await paginator.paginate('test/tool2', 'C'.repeat(200));
      const text = indexResult!.content[0]!.text;
      const match = /"_resultId": "([^"]+)"/.exec(text);
      expect(match).not.toBeNull();
      const resultId = match![1]!;

      const page1 = paginator.getPage(resultId, 1);
      expect(page1).not.toBeNull();
      expect(page1!.content[0]!.text).toContain('Page 1/');
      expect(page1!.content[0]!.text).toContain('C');
    });

    it('returns full content with _page=all', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'D'.repeat(200);
      const indexResult = await paginator.paginate('test/tool', raw);
      const match = /"_resultId": "([^"]+)"/.exec(indexResult!.content[0]!.text);
      const resultId = match![1]!;

      const allPages = paginator.getPage(resultId, 'all');
      expect(allPages).not.toBeNull();
      expect(allPages!.content[0]!.text).toBe(raw);
    });

    it('returns null for unknown resultId (cache miss)', () => {
      const paginator = new ResponsePaginator(null);
      const result = paginator.getPage('nonexistent-id', 1);
      expect(result).toBeNull();
    });

    it('returns error for out-of-range page', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'x'.repeat(200);
      const indexResult = await paginator.paginate('test/tool', raw);
      const match = /"_resultId": "([^"]+)"/.exec(indexResult!.content[0]!.text);
      const resultId = match![1]!;

      const page999 = paginator.getPage(resultId, 999);
      expect(page999).not.toBeNull();
      expect(page999!.content[0]!.text).toContain('out of range');
    });

    it('returns null after TTL expiry', async () => {
      const now = Date.now();
      vi.spyOn(Date, 'now').mockReturnValue(now);

      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50, ttlMs: 1000 });
      const raw = 'x'.repeat(200);
      const indexResult = await paginator.paginate('test/tool', raw);
      const match = /"_resultId": "([^"]+)"/.exec(indexResult!.content[0]!.text);
      const resultId = match![1]!;

      // Within TTL — should work
      expect(paginator.getPage(resultId, 1)).not.toBeNull();

      // Past TTL — should be null
      vi.spyOn(Date, 'now').mockReturnValue(now + 1001);
      expect(paginator.getPage(resultId, 1)).toBeNull();
    });
  });

  // --- extractPaginationParams ---

  describe('extractPaginationParams', () => {
    it('returns null when no pagination params', () => {
      expect(ResponsePaginator.extractPaginationParams({ query: 'test' })).toBeNull();
    });

    it('returns null when only _resultId (no _page)', () => {
      expect(ResponsePaginator.extractPaginationParams({ _resultId: 'abc' })).toBeNull();
    });

    it('returns null when only _page (no _resultId)', () => {
      expect(ResponsePaginator.extractPaginationParams({ _page: 1 })).toBeNull();
    });

    it('extracts numeric page', () => {
      const result = ResponsePaginator.extractPaginationParams({ _resultId: 'abc-123', _page: 2 });
      expect(result).toEqual({ resultId: 'abc-123', page: 2 });
    });

    it('extracts _page=all', () => {
      const result = ResponsePaginator.extractPaginationParams({ _resultId: 'abc-123', _page: 'all' });
      expect(result).toEqual({ resultId: 'abc-123', page: 'all' });
    });

    it('rejects negative page numbers', () => {
      expect(ResponsePaginator.extractPaginationParams({ _resultId: 'abc', _page: -1 })).toBeNull();
    });

    it('rejects zero page number', () => {
      expect(ResponsePaginator.extractPaginationParams({ _resultId: 'abc', _page: 0 })).toBeNull();
    });

    it('rejects non-integer page numbers', () => {
      expect(ResponsePaginator.extractPaginationParams({ _resultId: 'abc', _page: 1.5 })).toBeNull();
    });

    it('requires string resultId', () => {
      expect(ResponsePaginator.extractPaginationParams({ _resultId: 123, _page: 1 })).toBeNull();
    });
  });

  // --- Cache management ---

  describe('cache management', () => {
    it('evicts expired entries on paginate', async () => {
      const now = Date.now();
      vi.spyOn(Date, 'now').mockReturnValue(now);

      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50, ttlMs: 1000 });
      await paginator.paginate('test/tool1', 'x'.repeat(200));
      expect(paginator.cacheSize).toBe(1);

      // Advance past TTL and paginate again
      vi.spyOn(Date, 'now').mockReturnValue(now + 1001);
      await paginator.paginate('test/tool2', 'y'.repeat(200));
      // Old entry evicted, new one added
      expect(paginator.cacheSize).toBe(1);
    });

    it('evicts LRU at capacity', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50, maxCachedResults: 2 });
      await paginator.paginate('test/tool1', 'A'.repeat(200));
      await paginator.paginate('test/tool2', 'B'.repeat(200));
      expect(paginator.cacheSize).toBe(2);

      // Third entry should evict the first
      await paginator.paginate('test/tool3', 'C'.repeat(200));
      expect(paginator.cacheSize).toBe(2);
    });

    it('clearCache removes all entries', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      await paginator.paginate('test/tool1', 'x'.repeat(200));
      await paginator.paginate('test/tool2', 'y'.repeat(200));
      expect(paginator.cacheSize).toBe(2);

      paginator.clearCache();
      expect(paginator.cacheSize).toBe(0);
    });
  });

  // --- Page splitting ---

  describe('page splitting', () => {
    it('breaks at newline boundaries', async () => {
      // Create content where a newline falls within the page boundary
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 60 });
      const lines = Array.from({ length: 10 }, (_, i) => `line${String(i).padStart(3, '0')} ${'x'.repeat(20)}`);
      const raw = lines.join('\n');
      // raw is ~269 chars
      const result = await paginator.paginate('test/tool', raw);

      expect(result).not.toBeNull();
      // Pages should break at newline boundaries, not mid-line
      const text = result!.content[0]!.text;
      const match = /"_resultId": "([^"]+)"/.exec(text);
      const resultId = match![1]!;

      const page1 = paginator.getPage(resultId, 1);
      expect(page1).not.toBeNull();
      // Page content should end at a newline boundary (no partial lines)
      const pageText = page1!.content[0]!.text;
      // Remove the header line
      const contentStart = pageText.indexOf('\n\n') + 2;
      const pageContent = pageText.slice(contentStart);
      // Content should contain complete lines
      expect(pageContent).toMatch(/line\d{3}/);
    });

    it('handles content without newlines', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 50 });
      const raw = 'x'.repeat(200); // No newlines at all
      const result = await paginator.paginate('test/tool', raw);

      expect(result).not.toBeNull();
      const text = result!.content[0]!.text;
      expect(text).toContain('4 pages'); // 200/50 = 4
    });

    it('handles content that fits exactly in one page at threshold', async () => {
      const paginator = new ResponsePaginator(null, { sizeThreshold: 100, pageSize: 100 });
      const raw = 'x'.repeat(100); // Exactly at threshold and page size
      const result = await paginator.paginate('test/tool', raw);

      expect(result).not.toBeNull();
      const text = result!.content[0]!.text;
      expect(text).toContain('1 pages');
    });
  });
});
