import {
  afterAll,
  afterEach,
  beforeEach,
  describe,
  expect,
  mock,
  test,
} from 'bun:test'
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'

// Each test below calls `mock.module('axios', ...)` per-test. Without an
// afterAll cleanup, the LAST per-test stub leaks into every test file that
// runs after this one (mock.module is process-global, last-write-wins). The
// spread-real mock registered here at the end re-routes axios to the real
// module, undoing the stub leakage so later suites see real axios.
afterAll(() => {
  setupAxiosMock()
})

// Defensive mock: agent.test.ts mocks config.js which can corrupt Bun's
// src/* path alias resolution. Provide AbortError directly so the dynamic
// import in createAdapter() never needs to resolve the alias at runtime.
const _abortMock = () => ({
  AbortError: class AbortError extends Error {
    constructor(message?: string) {
      super(message)
      this.name = 'AbortError'
    }
  },
  isAbortError: (e: unknown) =>
    e instanceof Error && (e as Error).name === 'AbortError',
})
mock.module('src/utils/errors.js', _abortMock)
mock.module('src/utils/errors', _abortMock)

const originalBraveSearchApiKey = process.env.BRAVE_SEARCH_API_KEY
const originalBraveApiKey = process.env.BRAVE_API_KEY

describe('BraveSearchAdapter.search', () => {
  const createAdapter = async () => {
    const { BraveSearchAdapter } = await import('../adapters/braveAdapter')
    return new BraveSearchAdapter()
  }

  const SAMPLE_RESPONSE = {
    grounding: {
      generic: [
        {
          title: 'Result One',
          url: 'https://example.com/result1',
          snippets: ['Snippet one'],
        },
        {
          title: 'Result Two',
          url: 'https://example.com/result2',
          snippets: ['Snippet two'],
        },
      ],
    },
  }

  beforeEach(() => {
    process.env.BRAVE_SEARCH_API_KEY = 'test-brave-key'
    delete process.env.BRAVE_API_KEY
  })

  afterEach(() => {
    mock.restore()

    if (originalBraveSearchApiKey === undefined) {
      delete process.env.BRAVE_SEARCH_API_KEY
    } else {
      process.env.BRAVE_SEARCH_API_KEY = originalBraveSearchApiKey
    }

    if (originalBraveApiKey === undefined) {
      delete process.env.BRAVE_API_KEY
    } else {
      process.env.BRAVE_API_KEY = originalBraveApiKey
    }
  })

  test('returns parsed results from Brave LLM context payload', async () => {
    mock.module('axios', () => ({
      default: {
        get: mock(() => Promise.resolve({ data: SAMPLE_RESPONSE })),
        isCancel: () => false,
      },
    }))

    const adapter = await createAdapter()
    const results = await adapter.search('test query', {})

    expect(results).toHaveLength(2)
    expect(results[0]).toEqual({
      title: 'Result One',
      url: 'https://example.com/result1',
      snippet: 'Snippet one',
    })
    expect(results[1].title).toBe('Result Two')
  })

  test('calls onProgress with query_update and search_results_received', async () => {
    mock.module('axios', () => ({
      default: {
        get: mock(() => Promise.resolve({ data: SAMPLE_RESPONSE })),
        isCancel: () => false,
      },
    }))

    const progressCalls: any[] = []
    const onProgress = (p: any) => progressCalls.push(p)

    const adapter = await createAdapter()
    await adapter.search('test', { onProgress })

    expect(progressCalls).toHaveLength(2)
    expect(progressCalls[0]).toEqual({
      type: 'query_update',
      query: 'test',
    })
    expect(progressCalls[1]).toEqual({
      type: 'search_results_received',
      resultCount: 2,
      query: 'test',
    })
  })

  test('filters results by allowedDomains', async () => {
    const mixedResponse = {
      grounding: {
        generic: [
          { title: 'Allowed', url: 'https://allowed.com/a' },
          { title: 'Blocked', url: 'https://blocked.com/b' },
        ],
      },
    }

    mock.module('axios', () => ({
      default: {
        get: mock(() => Promise.resolve({ data: mixedResponse })),
        isCancel: () => false,
      },
    }))

    const adapter = await createAdapter()
    const results = await adapter.search('test', {
      allowedDomains: ['allowed.com'],
    })

    expect(results).toHaveLength(1)
    expect(results[0].url).toBe('https://allowed.com/a')
  })

  test('filters results by blockedDomains', async () => {
    const mixedResponse = {
      grounding: {
        generic: [
          { title: 'Good', url: 'https://good.com/a' },
          { title: 'Spam', url: 'https://spam.com/b' },
        ],
      },
    }

    mock.module('axios', () => ({
      default: {
        get: mock(() => Promise.resolve({ data: mixedResponse })),
        isCancel: () => false,
      },
    }))

    const adapter = await createAdapter()
    const results = await adapter.search('test', {
      blockedDomains: ['spam.com'],
    })

    expect(results).toHaveLength(1)
    expect(results[0].url).toBe('https://good.com/a')
  })

  test('filters subdomains with allowedDomains', async () => {
    const response = {
      grounding: {
        generic: [
          { title: 'Subdomain', url: 'https://docs.example.com/page' },
          { title: 'Other', url: 'https://other.com/page' },
        ],
      },
    }

    mock.module('axios', () => ({
      default: {
        get: mock(() => Promise.resolve({ data: response })),
        isCancel: () => false,
      },
    }))

    const adapter = await createAdapter()
    const results = await adapter.search('test', {
      allowedDomains: ['example.com'],
    })

    expect(results).toHaveLength(1)
    expect(results[0].url).toBe('https://docs.example.com/page')
  })

  test('throws AbortError when signal is already aborted', async () => {
    mock.module('axios', () => ({
      default: {
        get: mock((_url: string, config: any) => {
          if (config?.signal?.aborted) {
            const err = new Error('canceled')
            ;(err as any).__CANCEL__ = true
            return Promise.reject(err)
          }
          return Promise.resolve({ data: SAMPLE_RESPONSE })
        }),
        isCancel: (e: any) => e?.__CANCEL__ === true,
      },
    }))

    const adapter = await createAdapter()
    const controller = new AbortController()
    controller.abort()

    const { AbortError } = await import('src/utils/errors')
    await expect(
      adapter.search('test', { signal: controller.signal }),
    ).rejects.toThrow(AbortError)
  })

  test('re-throws non-abort axios errors', async () => {
    const networkError = new Error('Network error')
    mock.module('axios', () => ({
      default: {
        get: mock(() => Promise.reject(networkError)),
        isCancel: () => false,
      },
    }))

    const adapter = await createAdapter()
    await expect(adapter.search('test', {})).rejects.toThrow('Network error')
  })

  test('sends the documented HTTPS endpoint with query params and auth header', async () => {
    const axiosGet = mock(() => Promise.resolve({ data: SAMPLE_RESPONSE }))
    mock.module('axios', () => ({
      default: {
        get: axiosGet,
        isCancel: () => false,
      },
    }))

    const adapter = await createAdapter()
    await adapter.search('hello world & special=chars', {})

    expect(axiosGet.mock.calls).toHaveLength(1)
    expect((axiosGet.mock.calls as any[][])[0][0]).toBe(
      'https://api.search.brave.com/res/v1/llm/context',
    )
    expect((axiosGet.mock.calls as any[][])[0][1]).toMatchObject({
      params: { q: 'hello world & special=chars' },
      headers: {
        Accept: 'application/json',
        'X-Subscription-Token': 'test-brave-key',
      },
    })
  })

  test('accepts BRAVE_API_KEY as a fallback env var', async () => {
    delete process.env.BRAVE_SEARCH_API_KEY
    process.env.BRAVE_API_KEY = 'fallback-key'

    const axiosGet = mock(() => Promise.resolve({ data: SAMPLE_RESPONSE }))
    mock.module('axios', () => ({
      default: {
        get: axiosGet,
        isCancel: () => false,
      },
    }))

    const adapter = await createAdapter()
    await adapter.search('test', {})

    expect((axiosGet.mock.calls as any[][])[0][1].headers).toMatchObject({
      'X-Subscription-Token': 'fallback-key',
    })
  })

  test('throws when no Brave API key is configured', async () => {
    delete process.env.BRAVE_SEARCH_API_KEY
    delete process.env.BRAVE_API_KEY

    mock.module('axios', () => ({
      default: {
        get: mock(() => Promise.resolve({ data: SAMPLE_RESPONSE })),
        isCancel: () => false,
      },
    }))

    const adapter = await createAdapter()
    await expect(adapter.search('test', {})).rejects.toThrow(
      'BraveSearchAdapter requires BRAVE_SEARCH_API_KEY or BRAVE_API_KEY',
    )
  })
})
