import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createWebSearchTool } from './web-search.js';
import type { WebSearchConfig } from './web-search.js';

// Mock global fetch
const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);

beforeEach(() => {
  mockFetch.mockReset();
});

// ── Helper: build mock Response objects ──────────────────────────────────────

function mockJsonResponse(body: unknown, status = 200) {
  return {
    ok: status >= 200 && status < 300,
    status,
    text: async () => JSON.stringify(body),
    json: async () => body,
    headers: new Headers({ 'content-type': 'application/json' }),
  };
}

// ── Brave mock data ──────────────────────────────────────────────────────────

const braveResults = {
  web: {
    results: [
      { title: 'Result 1', url: 'https://example.com/1', description: 'Description 1' },
      { title: 'Result 2', url: 'https://example.com/2', description: 'Description 2' },
      { title: 'Result 3', url: 'https://example.com/3', description: 'Description 3' },
      { title: 'Result 4', url: 'https://example.com/4', description: 'Description 4' },
      { title: 'Result 5', url: 'https://example.com/5', description: 'Description 5' },
    ],
  },
};

// ── SearXNG mock data ────────────────────────────────────────────────────────

const searxngResults = {
  results: [
    { title: 'Result 1', url: 'https://example.com/1', content: 'Content 1' },
    { title: 'Result 2', url: 'https://example.com/2', content: 'Content 2' },
    { title: 'Result 3', url: 'https://example.com/3', content: 'Content 3' },
    { title: 'Result 4', url: 'https://example.com/4', content: 'Content 4' },
    { title: 'Result 5', url: 'https://example.com/5', content: 'Content 5' },
  ],
};

// ── Brave config ─────────────────────────────────────────────────────────────

const braveConfig: WebSearchConfig = {
  provider: 'brave',
  apiKey: 'test-brave-key',
};

// ── SearXNG config ───────────────────────────────────────────────────────────

const searxngConfig: WebSearchConfig = {
  provider: 'searxng',
  endpoint: 'http://searxng:8080',
};

// ═════════════════════════════════════════════════════════════════════════════
// Tests
// ═════════════════════════════════════════════════════════════════════════════

describe('web.search', () => {
  // ── Metadata ─────────────────────────────────────────────────────────────

  it('has correct metadata', () => {
    const tool = createWebSearchTool(braveConfig);
    expect(tool.name).toBe('web.search');
    expect(tool.description).toBeTruthy();
    expect(tool.inputSchema.required).toContain('query');
  });

  // ── Brave provider ──────────────────────────────────────────────────────

  describe('brave provider', () => {
    it('returns formatted results on successful search', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse(braveResults));
      const tool = createWebSearchTool(braveConfig);

      const result = await tool.execute({ query: 'test query' });

      expect(result.success).toBe(true);
      expect(result.output).toContain('**Result 1**');
      expect(result.output).toContain('https://example.com/1');
      expect(result.output).toContain('Description 1');
      expect(result.output).toContain('**Result 2**');
      expect(result.output).toContain('https://example.com/2');
      expect(result.output).toContain('Description 2');

      // Verify fetch was called with correct URL and headers
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('api.search.brave.com'),
        expect.objectContaining({
          headers: expect.objectContaining({
            'X-Subscription-Token': 'test-brave-key',
          }),
        }),
      );
    });

    it('handles empty results', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse({ web: { results: [] } }));
      const tool = createWebSearchTool(braveConfig);

      const result = await tool.execute({ query: 'obscure query' });

      expect(result.success).toBe(true);
      expect(result.output).toBe('No results found for: obscure query');
    });

    it('returns error when no API key configured', async () => {
      const tool = createWebSearchTool({ provider: 'brave' });

      const result = await tool.execute({ query: 'test' });

      expect(result.success).toBe(false);
      expect(result.error).toBe('Brave Search API key not configured');
      expect(mockFetch).not.toHaveBeenCalled();
    });

    it('handles HTTP errors from Brave API', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse({ error: 'rate limited' }, 429));
      const tool = createWebSearchTool(braveConfig);

      const result = await tool.execute({ query: 'test' });

      expect(result.success).toBe(false);
      expect(result.error).toContain('429');
    });

    it('handles network errors', async () => {
      mockFetch.mockRejectedValue(new Error('Failed to fetch'));
      const tool = createWebSearchTool(braveConfig);

      const result = await tool.execute({ query: 'test' });

      expect(result.success).toBe(false);
      expect(result.error).toContain('Failed to fetch');
    });
  });

  // ── SearXNG provider ────────────────────────────────────────────────────

  describe('searxng provider', () => {
    it('returns formatted results on successful search', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse(searxngResults));
      const tool = createWebSearchTool(searxngConfig);

      const result = await tool.execute({ query: 'test query' });

      expect(result.success).toBe(true);
      expect(result.output).toContain('**Result 1**');
      expect(result.output).toContain('https://example.com/1');
      expect(result.output).toContain('Content 1');
      expect(result.output).toContain('**Result 2**');

      // Verify fetch was called with correct URL
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('http://searxng:8080/search'),
        expect.any(Object),
      );
      // Verify query params include format=json
      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain('format=json');
      expect(calledUrl).toContain('categories=general');
    });

    it('handles empty results', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse({ results: [] }));
      const tool = createWebSearchTool(searxngConfig);

      const result = await tool.execute({ query: 'nothing here' });

      expect(result.success).toBe(true);
      expect(result.output).toBe('No results found for: nothing here');
    });

    it('returns error when no endpoint configured', async () => {
      const tool = createWebSearchTool({ provider: 'searxng' });

      const result = await tool.execute({ query: 'test' });

      expect(result.success).toBe(false);
      expect(result.error).toBe('SearXNG endpoint not configured');
      expect(mockFetch).not.toHaveBeenCalled();
    });
  });

  // ── Count parameter ─────────────────────────────────────────────────────

  describe('count parameter', () => {
    it('limits results to the requested count', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse(braveResults));
      const tool = createWebSearchTool(braveConfig);

      const result = await tool.execute({ query: 'test', count: 2 });

      expect(result.success).toBe(true);
      // Verify count=2 was passed to the Brave API
      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain('count=2');
    });

    it('caps count at 20', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse(braveResults));
      const tool = createWebSearchTool(braveConfig);

      await tool.execute({ query: 'test', count: 50 });

      // Verify count was capped at 20
      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain('count=20');
    });

    it('uses default count when not specified', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse(braveResults));
      const tool = createWebSearchTool(braveConfig);

      await tool.execute({ query: 'test' });

      // Default count is 5
      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain('count=5');
    });

    it('respects maxResults from config', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse(braveResults));
      const tool = createWebSearchTool({ ...braveConfig, maxResults: 3 });

      await tool.execute({ query: 'test' });

      const calledUrl = mockFetch.mock.calls[0][0] as string;
      expect(calledUrl).toContain('count=3');
    });
  });

  // ── Output format ───────────────────────────────────────────────────────

  describe('output format', () => {
    it('formats results as numbered markdown', async () => {
      mockFetch.mockResolvedValue(mockJsonResponse(braveResults));
      const tool = createWebSearchTool(braveConfig);

      const result = await tool.execute({ query: 'test', count: 2 });

      expect(result.success).toBe(true);
      // Check numbered format with em-dash separator
      expect(result.output).toMatch(/^1\. \*\*Result 1\*\* \u2014 https:\/\/example\.com\/1/);
      expect(result.output).toContain('2. **Result 2**');
    });
  });
});
