// `ApiContext.fetchUrl` host-gate tests. The URL passed to `fetchUrl` is a
// `Link:` pagination header value the home server fully controls — a hostile
// or compromised server could point `next` / `prev` cursors at an attacker
// domain and harvest the bearer that `fetchUrl` attaches as `Authorization`.
//
// The gate:
//   - HTTPS only (no `http:`, `data:`, `javascript:`, …)
//   - hostname must match `instanceHostName` (case-insensitive)
//   - malformed URLs throw before any token leaves the device
//
// Pattern mirrors `__tests__/accountStore.ts` — react-native-mmkv and
// expo-secure-store are stubbed out so the storage transitive imports load
// cleanly under jest, and `jest.isolateModules` keeps module state per-test.

import { createMmkvInstances } from './_helpers/mockMmkv'
import { createSecureStoreMock } from './_helpers/mockSecureStore'

const mockMmkv = createMmkvInstances()
const mockSecure = createSecureStoreMock()

jest.mock('react-native-mmkv', () => ({
  createMMKV: ({
    id,
    compareBeforeSet,
  }: {
    id: string
    compareBeforeSet?: boolean
  }) => mockMmkv.getOrCreate(id, { compareBeforeSet }),
}))

jest.mock('expo-secure-store', () => ({
  getItem: (key: string) => mockSecure.getItem(key),
  setItem: (key: string, value: string) => mockSecure.setItem(key, value),
  deleteItemAsync: (key: string) => mockSecure.deleteItemAsync(key),
}))

type ApiContextModule = typeof import('../src/lib/api-context')

function loadApiContext(): ApiContextModule {
  let mod: ApiContextModule | null = null
  jest.isolateModules(() => {
    mod = require('../src/lib/api-context') as ApiContextModule
  })
  return mod!
}

const HOST = 'pixelfed.example'
const TOKEN = 'token-abc'
const ACCOUNT_ID = 'acc-1'

beforeEach(() => {
  mockMmkv.reset()
  mockSecure.reset()
  jest.restoreAllMocks()
})

describe('ApiContext.fetchUrl host gate', () => {
  it('refuses http:// pagination URLs even when the host matches', async () => {
    const { ApiContext, ApiError } = loadApiContext()
    const ctx = new ApiContext(HOST, TOKEN, ACCOUNT_ID, 'pixelfed')
    const fetchSpy = jest.spyOn(global, 'fetch')

    await expect(ctx.fetchUrl(`http://${HOST}/api/v1/timelines/home?cursor=2`)).rejects.toBeInstanceOf(ApiError)
    expect(fetchSpy).not.toHaveBeenCalled()
  })

  it('refuses https:// URLs whose host does not match the configured instance', async () => {
    const { ApiContext, ApiError } = loadApiContext()
    const ctx = new ApiContext(HOST, TOKEN, ACCOUNT_ID, 'pixelfed')
    const fetchSpy = jest.spyOn(global, 'fetch')

    await expect(
      ctx.fetchUrl('https://attacker.example/api/v1/timelines/home?cursor=2')
    ).rejects.toBeInstanceOf(ApiError)
    expect(fetchSpy).not.toHaveBeenCalled()
  })

  it('throws ApiError with the cross-host code so callers can branch on it', async () => {
    const { ApiContext } = loadApiContext()
    const ctx = new ApiContext(HOST, TOKEN, ACCOUNT_ID, 'pixelfed')

    await expect(
      ctx.fetchUrl('https://attacker.example/foo')
    ).rejects.toMatchObject({ code: 'cross-host-pagination' })
  })

  it('refuses a malformed URL before any fetch fires', async () => {
    const { ApiContext, ApiError } = loadApiContext()
    const ctx = new ApiContext(HOST, TOKEN, ACCOUNT_ID, 'pixelfed')
    const fetchSpy = jest.spyOn(global, 'fetch')

    await expect(ctx.fetchUrl('not a url')).rejects.toBeInstanceOf(ApiError)
    expect(fetchSpy).not.toHaveBeenCalled()
  })

  it('passes through happy-path URLs to fetch with the bearer attached', async () => {
    const { ApiContext } = loadApiContext()
    const ctx = new ApiContext(HOST, TOKEN, ACCOUNT_ID, 'pixelfed')
    const fetchSpy = jest
      .spyOn(global, 'fetch')
      .mockResolvedValue(new Response('[]', { status: 200 }))

    await ctx.fetchUrl(`https://${HOST}/api/v1/timelines/home?cursor=2`)

    expect(fetchSpy).toHaveBeenCalledTimes(1)
    const [calledUrl, init] = fetchSpy.mock.calls[0] as [string, RequestInit]
    expect(calledUrl.startsWith(`https://${HOST}/api/v1/timelines/home`)).toBe(true)
    // _pe=1 added on Pixelfed contexts, mirrors buildUrl
    expect(calledUrl.includes('_pe=1')).toBe(true)
    expect((init.headers as Record<string, string>).Authorization).toBe(`Bearer ${TOKEN}`)
  })

  it('matches hostname case-insensitively', async () => {
    const { ApiContext } = loadApiContext()
    const ctx = new ApiContext(HOST, TOKEN, ACCOUNT_ID, 'pixelfed')
    jest
      .spyOn(global, 'fetch')
      .mockResolvedValue(new Response('[]', { status: 200 }))

    // Server pagination cursors are sometimes rewritten with mixed-case
    // hostnames; DNS is case-insensitive so this must be allowed.
    await expect(
      ctx.fetchUrl(`https://${HOST.toUpperCase()}/api/v1/timelines/home?cursor=3`)
    ).resolves.toBeDefined()
  })
})
