// Favicon module behaviour lock-down. Covers the four invariants the inline
// + chip favicon paths rely on: (1) probe writes `available` on an image
// response, (2) probe writes `unavailable` on a 404, (3) probe writes
// `unavailable` on an HTML 200 (the SPA-index-page false-positive trap),
// (4) parallel probes for the same host coalesce into one fetch, and (5) a
// stale `unavailable` entry re-probes after 30 days.

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

const mockMmkv = createMmkvInstances()

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

type FaviconModule = typeof import('../src/lib/favicon')

function loadModule(): FaviconModule {
  let mod: FaviconModule | null = null
  jest.isolateModules(() => {
    mod = require('../src/lib/favicon') as FaviconModule
  })
  return mod!
}

function mockFetch(impl: (url: string, init?: any) => Promise<Response>) {
  return jest.spyOn(global, 'fetch').mockImplementation(impl as any)
}

function respond(status: number, contentType: string): Response {
  return {
    ok: status >= 200 && status < 300,
    status,
    headers: {
      get: (h: string) => (h.toLowerCase() === 'content-type' ? contentType : null),
    },
  } as unknown as Response
}

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

describe('probeFavicon', () => {
  it('writes available on 200 image/png', async () => {
    const { probeFavicon, getFaviconStatus } = loadModule()
    mockFetch(async () => respond(200, 'image/png'))

    const status = await probeFavicon('example.com')

    expect(status).toBe('available')
    expect(getFaviconStatus('example.com')).toBe('available')
  })

  it('writes available on 200 image/x-icon', async () => {
    const { probeFavicon, getFaviconStatus } = loadModule()
    mockFetch(async () => respond(200, 'image/vnd.microsoft.icon'))

    const status = await probeFavicon('example.com')

    expect(status).toBe('available')
    expect(getFaviconStatus('example.com')).toBe('available')
  })

  it('writes unavailable on 404', async () => {
    const { probeFavicon, getFaviconStatus } = loadModule()
    mockFetch(async () => respond(404, 'text/html'))

    const status = await probeFavicon('missing.example.com')

    expect(status).toBe('unavailable')
    expect(getFaviconStatus('missing.example.com')).toBe('unavailable')
  })

  it('writes unavailable on text/html 200 (SPA index trap)', async () => {
    const { probeFavicon, getFaviconStatus } = loadModule()
    mockFetch(async () => respond(200, 'text/html; charset=utf-8'))

    const status = await probeFavicon('spa.example.com')

    expect(status).toBe('unavailable')
    expect(getFaviconStatus('spa.example.com')).toBe('unavailable')
  })

  it('writes unavailable when fetch throws', async () => {
    const { probeFavicon, getFaviconStatus } = loadModule()
    mockFetch(async () => {
      throw new Error('network down')
    })

    const status = await probeFavicon('offline.example.com')

    expect(status).toBe('unavailable')
    expect(getFaviconStatus('offline.example.com')).toBe('unavailable')
  })

  it('coalesces parallel calls for the same host into one fetch', async () => {
    const { probeFavicon } = loadModule()
    let calls = 0
    mockFetch(async () => {
      calls++
      // Yield once so both callers actually queue up before the first resolves.
      await new Promise<void>((resolve) => setImmediate(resolve))
      return respond(200, 'image/png')
    })

    const [a, b, c] = await Promise.all([
      probeFavicon('coalesce.example.com'),
      probeFavicon('coalesce.example.com'),
      probeFavicon('coalesce.example.com'),
    ])

    expect([a, b, c]).toEqual(['available', 'available', 'available'])
    expect(calls).toBe(1)
  })

  it('falls back to GET when HEAD returns 405', async () => {
    const { probeFavicon } = loadModule()
    const methods: string[] = []
    mockFetch(async (_url, init) => {
      methods.push((init?.method as string) ?? 'GET')
      if (init?.method === 'HEAD') return respond(405, 'text/html')
      return respond(200, 'image/png')
    })

    const status = await probeFavicon('head-allergic.example.com')

    expect(status).toBe('available')
    expect(methods).toEqual(['HEAD', 'GET'])
  })
})

describe('getFaviconStatus', () => {
  it('reports unknown when nothing has been written', () => {
    const { getFaviconStatus } = loadModule()
    expect(getFaviconStatus('never-seen.example.com')).toBe('unknown')
  })

  it('re-probes unavailable after 30 days', async () => {
    const { probeFavicon, getFaviconStatus } = loadModule()
    mockFetch(async () => respond(404, 'text/html'))
    await probeFavicon('stale.example.com')
    expect(getFaviconStatus('stale.example.com')).toBe('unavailable')

    // Advance the checkedAt timestamp into the past beyond the 30-day window.
    const app = mockMmkv.get('app')!
    app.set(
      'app.favicon.checkedAt.stale.example.com',
      Date.now() - 31 * 24 * 60 * 60 * 1000
    )

    expect(getFaviconStatus('stale.example.com')).toBe('unknown')
  })
})
