import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { CachedContent, CacheStats } from '../../../src/types.js';

vi.mock('../../../src/cache/store.js', () => ({
  searchCacheFiltered: vi.fn(),
  getCacheStats: vi.fn(),
  clearCacheEntries: vi.fn(),
}));

vi.mock('../../../src/logger.js', () => ({
  createLogger: () => ({
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  }),
}));

vi.mock('../../../src/cache/change-detector.js', () => ({
  detectChange: vi.fn(),
}));

const extractMock = vi.fn();
vi.mock('../../../src/providers/extract-provider.js', () => ({
  getExtractProvider: vi.fn(async () => ({
    name: 'v1' as const,
    extract: extractMock,
  })),
  _resetExtractProviderForTest: vi.fn(),
}));


import { handleCache } from '../../../src/tools/cache.js';
import { searchCacheFiltered, getCacheStats, clearCacheEntries } from '../../../src/cache/store.js';
import { detectChange } from '../../../src/cache/change-detector.js';

function mockRouter(html = '<html></html>', finalUrl?: string) {
  return {
    fetch: vi.fn().mockResolvedValue({
      url: finalUrl ?? 'https://example.com',
      finalUrl: finalUrl ?? 'https://example.com',
      html,
      contentType: 'text/html',
      statusCode: 200,
      headers: {},
    }),
  } as any;
}

function makeCachedContent(overrides: Partial<CachedContent> = {}): CachedContent {
  return {
    id: 1,
    url: 'https://example.com',
    normalizedUrl: 'https://example.com',
    title: 'Example',
    markdown: '# Example\n\nContent here.',
    rawHtml: '<html></html>',
    metadata: '{}',
    links: '[]',
    images: '[]',
    fetchMethod: 'http',
    extractorUsed: 'defuddle',
    contentHash: 'abc123',
    fetchedAt: '2026-04-12 10:00:00',
    expiresAt: null,
    ...overrides,
  };
}

describe('handleCache', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('returns stats when stats=true', async () => {
    const stats: CacheStats = {
      total_urls: 5,
      total_size_mb: 1.23,
      oldest: '2026-04-10 00:00:00',
      newest: '2026-04-12 00:00:00',
    };
    vi.mocked(getCacheStats).mockReturnValue(stats);

    const result = await handleCache({ stats: true });

    expect(result.stats).toEqual(stats);
    expect(result.results).toBeUndefined();
    expect(result.cleared).toBeUndefined();
    expect(getCacheStats).toHaveBeenCalledOnce();
  });

  it('returns cleared count when clear=true', async () => {
    vi.mocked(clearCacheEntries).mockReturnValue(3);

    const result = await handleCache({ clear: true, url_pattern: '*example.com*' });

    expect(result.cleared).toBe(3);
    expect(result.results).toBeUndefined();
    expect(clearCacheEntries).toHaveBeenCalledWith({
      query: undefined,
      urlPattern: '*example.com*',
      since: undefined,
    });
  });

  it('returns search results for query', async () => {
    const cached = [makeCachedContent()];
    vi.mocked(searchCacheFiltered).mockReturnValue(cached);

    const result = await handleCache({ query: 'example' });

    expect(result.results).toHaveLength(1);
    expect(result.results![0].url).toBe('https://example.com');
    expect(result.results![0].title).toBe('Example');
    expect(result.results![0].markdown).toBe('# Example\n\nContent here.');
    expect(result.results![0].fetched_at).toBe('2026-04-12 10:00:00');
    // cache.query default limit is 5 to keep response token-bounded
    expect(searchCacheFiltered).toHaveBeenCalledWith({
      query: 'example',
      urlPattern: undefined,
      since: undefined,
      limit: 5,
    });
  });

  it('passes all filters to searchCacheFiltered', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([]);

    await handleCache({ query: 'test', url_pattern: '*docs*', since: '2026-04-01' });

    // cache.query default limit is 5 to keep response token-bounded
    expect(searchCacheFiltered).toHaveBeenCalledWith({
      query: 'test',
      urlPattern: '*docs*',
      since: '2026-04-01',
      limit: 5,
    });
  });

  it('returns empty results for no matches', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([]);

    const result = await handleCache({ query: 'nonexistent' });

    expect(result.results).toEqual([]);
  });

  it('returns error on exception', async () => {
    vi.mocked(searchCacheFiltered).mockImplementation(() => {
      throw new Error('DB error');
    });

    const result = await handleCache({ query: 'test' });

    expect(result.error).toBe('DB error');
  });

  it('rejects clear without filters', async () => {
    const result = await handleCache({ clear: true });

    expect(result.error).toBe('clear requires at least one filter (query, url_pattern, or since)');
    expect(result.cleared).toBeUndefined();
    expect(clearCacheEntries).not.toHaveBeenCalled();
  });

  it('clears with combined query + url_pattern', async () => {
    vi.mocked(clearCacheEntries).mockReturnValue(2);

    const result = await handleCache({ clear: true, query: 'test', url_pattern: '*example.com*' });

    expect(result.cleared).toBe(2);
    expect(clearCacheEntries).toHaveBeenCalledWith({
      query: 'test',
      urlPattern: '*example.com*',
      since: undefined,
    });
  });

  it('stats takes priority over clear', async () => {
    const stats: CacheStats = { total_urls: 1, total_size_mb: 0.01, oldest: '', newest: '' };
    vi.mocked(getCacheStats).mockReturnValue(stats);

    const result = await handleCache({ stats: true, clear: true });

    expect(result.stats).toBeDefined();
    expect(result.cleared).toBeUndefined();
    expect(clearCacheEntries).not.toHaveBeenCalled();
  });

  it('caps total markdown tokens to max_tokens_out across results', async () => {
    const big = (n: number) => 'word '.repeat(n).trim();
    vi.mocked(searchCacheFiltered).mockReturnValue([
      makeCachedContent({ url: 'https://a.example.com', markdown: big(800) }),
      makeCachedContent({ url: 'https://b.example.com', markdown: big(800) }),
      makeCachedContent({ url: 'https://c.example.com', markdown: big(800) }),
    ]);
    const { countTokens } = await import('../../../src/search/tokens.js');

    const result = await handleCache({ query: 'word', max_tokens_out: 200 });
    expect(result.results).toBeDefined();
    const total = result.results!.reduce((n, r) => n + countTokens(r.markdown), 0);
    expect(total).toBeLessThanOrEqual(220);
  });

  it('passes limit to searchCacheFiltered', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([]);
    await handleCache({ query: 'q', limit: 7 });
    expect(searchCacheFiltered).toHaveBeenCalledWith(expect.objectContaining({ limit: 7 }));
  });
});

describe('handleCache --- check_changes mode', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(detectChange).mockReturnValue({ changed: false });
    extractMock.mockResolvedValue({
      title: 'Test',
      markdown: 'new content',
      metadata: {},
      links: [],
      images: [],
    });
  });

  it('returns changes array when check_changes is true', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([
      {
        id: 1,
        url: 'https://example.com/a',
        normalizedUrl: 'https://example.com/a',
        title: 'Page A',
        markdown: 'content A',
        rawHtml: '',
        metadata: '{}',
        links: '[]',
        images: '[]',
        fetchMethod: 'http' as const,
        extractorUsed: 'defuddle' as const,
        contentHash: 'abc123',
        fetchedAt: '2026-04-14T00:00:00',
        expiresAt: null,
      },
    ]);

    vi.mocked(detectChange).mockReturnValue({ changed: false });
    const router = mockRouter();

    const result = await handleCache({
      check_changes: true,
      url_pattern: '*example.com*',
    }, router);

    expect(result.changes).toBeDefined();
    expect(result.changes).toHaveLength(1);
    expect(result.changes![0].url).toBe('https://example.com/a');
    expect(result.changes![0].changed).toBe(false);
    expect(router.fetch).toHaveBeenCalledWith('https://example.com/a', { renderJs: 'auto' });
  });

  it('reports changed=true for URLs with different content', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([
      {
        id: 1,
        url: 'https://example.com/b',
        normalizedUrl: 'https://example.com/b',
        title: 'Page B',
        markdown: 'old content',
        rawHtml: '',
        metadata: '{}',
        links: '[]',
        images: '[]',
        fetchMethod: 'http' as const,
        extractorUsed: 'defuddle' as const,
        contentHash: 'old-hash',
        fetchedAt: '2026-04-14T00:00:00',
        expiresAt: null,
      },
    ]);

    vi.mocked(detectChange).mockReturnValue({
      changed: true,
      previousHash: 'old-hash',
      diffSummary: '3 lines added, 1 line removed, 0 lines modified',
    });
    const router = mockRouter();

    const result = await handleCache({
      check_changes: true,
      url_pattern: '*example.com*',
    }, router);

    expect(result.changes![0].changed).toBe(true);
    expect(result.changes![0].previous_hash).toBe('old-hash');
    expect(result.changes![0].diff_summary).toContain('3 lines added');
  });

  it('handles multiple cached URLs', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([
      {
        id: 1, url: 'https://a.com', normalizedUrl: 'https://a.com',
        title: 'A', markdown: 'a', rawHtml: '', metadata: '{}',
        links: '[]', images: '[]', fetchMethod: 'http' as const,
        extractorUsed: 'defuddle' as const, contentHash: 'ha',
        fetchedAt: '2026-04-14T00:00:00', expiresAt: null,
      },
      {
        id: 2, url: 'https://b.com', normalizedUrl: 'https://b.com',
        title: 'B', markdown: 'b', rawHtml: '', metadata: '{}',
        links: '[]', images: '[]', fetchMethod: 'http' as const,
        extractorUsed: 'defuddle' as const, contentHash: 'hb',
        fetchedAt: '2026-04-14T00:00:00', expiresAt: null,
      },
    ]);

    vi.mocked(detectChange)
      .mockReturnValueOnce({ changed: false })
      .mockReturnValueOnce({ changed: true, previousHash: 'hb', diffSummary: '1 line added' });

    const result = await handleCache({ check_changes: true }, mockRouter());

    expect(result.changes).toHaveLength(2);
    expect(result.changes![0].changed).toBe(false);
    expect(result.changes![1].changed).toBe(true);
  });

  it('returns empty changes array when no cached entries match', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([]);

    const result = await handleCache({ check_changes: true, url_pattern: '*nonexistent*' }, mockRouter());

    expect(result.changes).toBeDefined();
    expect(result.changes).toHaveLength(0);
  });

  it('uses query and url_pattern to scope entries', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([]);

    await handleCache({
      check_changes: true,
      query: 'react',
      url_pattern: '*docs*',
    }, mockRouter());

    expect(vi.mocked(searchCacheFiltered)).toHaveBeenCalledWith(
      expect.objectContaining({
        query: 'react',
        urlPattern: '*docs*',
      }),
    );
  });

  it('handles fetch error for individual URLs', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([
      {
        id: 1, url: 'https://error.com', normalizedUrl: 'https://error.com',
        title: 'Error Page', markdown: 'content', rawHtml: '', metadata: '{}',
        links: '[]', images: '[]', fetchMethod: 'http' as const,
        extractorUsed: 'defuddle' as const, contentHash: 'h',
        fetchedAt: '2026-04-14T00:00:00', expiresAt: null,
      },
    ]);

    const router = { fetch: vi.fn().mockRejectedValue(new Error('Network error')) } as any;

    const result = await handleCache({ check_changes: true }, router);

    expect(result.changes).toHaveLength(1);
    expect(result.changes![0].error).toContain('Network error');
    expect(result.changes![0].changed).toBe(false);
  });

  it('check_changes takes priority over stats', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([]);

    const result = await handleCache({ check_changes: true, stats: true }, mockRouter());

    expect(result.changes).toBeDefined();
    expect(result.stats).toBeUndefined();
  });

  it('check_changes takes priority over clear', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([]);

    const result = await handleCache({ check_changes: true, clear: true }, mockRouter());

    expect(result.changes).toBeDefined();
    expect(result.cleared).toBeUndefined();
  });

  it('includes current_hash in change report', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([
      {
        id: 1, url: 'https://example.com/c', normalizedUrl: 'https://example.com/c',
        title: 'C', markdown: 'content', rawHtml: '', metadata: '{}',
        links: '[]', images: '[]', fetchMethod: 'http' as const,
        extractorUsed: 'defuddle' as const, contentHash: 'current-h',
        fetchedAt: '2026-04-14T00:00:00', expiresAt: null,
      },
    ]);

    vi.mocked(detectChange).mockReturnValue({ changed: false });

    const result = await handleCache({ check_changes: true }, mockRouter());

    expect(result.changes![0].current_hash).toBe('current-h');
  });

  it('reports error when no router provided', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([
      {
        id: 1, url: 'https://example.com', normalizedUrl: 'https://example.com',
        title: 'X', markdown: 'x', rawHtml: '', metadata: '{}',
        links: '[]', images: '[]', fetchMethod: 'http' as const,
        extractorUsed: 'defuddle' as const, contentHash: 'h',
        fetchedAt: '2026-04-14T00:00:00', expiresAt: null,
      },
    ]);

    const result = await handleCache({ check_changes: true });

    expect(result.changes![0].error).toBe('no router available for re-fetch');
    expect(result.changes![0].changed).toBe(false);
  });

  it('passes re-fetched content to detectChange', async () => {
    vi.mocked(searchCacheFiltered).mockReturnValue([
      {
        id: 1, url: 'https://example.com', normalizedUrl: 'https://example.com',
        title: 'X', markdown: 'old', rawHtml: '', metadata: '{}',
        links: '[]', images: '[]', fetchMethod: 'http' as const,
        extractorUsed: 'defuddle' as const, contentHash: 'h',
        fetchedAt: '2026-04-14T00:00:00', expiresAt: null,
      },
    ]);

    extractMock.mockResolvedValue({
      title: 'X',
      markdown: 'freshly fetched content',
      metadata: {},
      links: [],
      images: [],
    });

    const result = await handleCache({ check_changes: true }, mockRouter());

    // cache check_changes now forwards the upstream status
    // code so detectChange can flag 200→404 status flips as changes.
    expect(vi.mocked(detectChange)).toHaveBeenCalledWith(
      'https://example.com',
      'freshly fetched content',
      expect.any(Number),
    );
  });
});