// Tapestry live-tail poll tests. Verifies the 3 s tick cadence, the
// background-stops-polling guard, and that the bucket path is computed
// against `currentBucket()`. Other timing-sensitive cases (focus blur,
// minute roll-over) are exercised in manual QA — the hook's lifecycle
// surface is too tied to the react-navigation focus-effect contract for a
// clean unit test, and unit-testing them would mostly verify the mocks.

import React from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'

const mockState = {
  request: jest.fn() as jest.Mock,
  appStateListener: null as ((s: string) => void) | null,
}

jest.mock('react-native', () => ({
  AppState: {
    currentState: 'active',
    addEventListener: (_: string, fn: (s: string) => void) => {
      mockState.appStateListener = fn
      return {
        remove: () => {
          mockState.appStateListener = null
        },
      }
    },
  },
}))

jest.mock('expo-router', () => {
  // useFocusEffect is exposed via expo-router's re-export of
  // @react-navigation/native; the hook treats the screen as focused
  // immediately on mount and runs the returned cleanup on unmount. The mock
  // mirrors that contract via a regular React effect — sufficient for the
  // smoke-level checks below.
  const React = require('react') as typeof import('react')
  return {
    useFocusEffect: (cb: () => void | (() => void)) => {
      React.useEffect(() => {
        const cleanup = cb()
        return typeof cleanup === 'function' ? cleanup : undefined
      }, [cb])
    },
  }
})

jest.mock('src/lib/api-context', () => ({
  ContextForActiveAccount: () => ({ request: mockState.request }),
}))

// Lazy import so module-load happens AFTER the mocks above are registered.
// `require` rather than `import` keeps the load deferred without TS lint
// griping about top-level imports.
const { useTapestryPoll } =
  require('src/lib/tapestry/poll') as typeof import('src/lib/tapestry/poll')

let probeResult: { liveBucket: string; liveLetters: string } | null = null

function Probe({ host }: { host: string }) {
  probeResult = useTapestryPoll(host)
  return null
}

function jsonResponse(rows: { bucket: string; letters: string }[]) {
  return {
    json: async () => rows,
  } as unknown as Response
}

// Pin to a fixed UTC instant so `currentBucket()` is deterministic. The
// minute is 12:30 of 2026-04-28 — a 30-min page boundary, which matches
// the `tapestry-window` round-trip cases.
const FIXED_NOW = Date.UTC(2026, 3, 28, 12, 30, 0, 0)

describe('useTapestryPoll', () => {
  beforeEach(() => {
    jest.useFakeTimers({ now: FIXED_NOW })
    mockState.request.mockReset()
    mockState.appStateListener = null
    probeResult = null
  })

  afterEach(() => {
    jest.useRealTimers()
  })

  it('polls the current bucket on mount', async () => {
    mockState.request.mockResolvedValue(
      jsonResponse([{ bucket: '202604281230', letters: 'L' }])
    )
    let root: ReactTestRenderer
    await act(async () => {
      root = create(React.createElement(Probe, { host: 'example.test' }))
    })
    expect(mockState.request).toHaveBeenCalledTimes(1)
    expect(mockState.request).toHaveBeenLastCalledWith(
      '/api/v1/tapestry/bucket/202604281230',
      { method: 'GET' }
    )
    await act(async () => {
      root!.unmount()
    })
  })

  it('continues polling every 3 s while focused + foregrounded', async () => {
    mockState.request.mockResolvedValue(jsonResponse([]))
    let root: ReactTestRenderer
    await act(async () => {
      root = create(React.createElement(Probe, { host: 'example.test' }))
    })
    expect(mockState.request).toHaveBeenCalledTimes(1)
    await act(async () => {
      jest.advanceTimersByTime(3_000)
    })
    expect(mockState.request).toHaveBeenCalledTimes(2)
    await act(async () => {
      jest.advanceTimersByTime(3_000)
    })
    expect(mockState.request).toHaveBeenCalledTimes(3)
    await act(async () => {
      root!.unmount()
    })
  })

  it('stops fetching after the app moves to background', async () => {
    mockState.request.mockResolvedValue(jsonResponse([]))
    let root: ReactTestRenderer
    await act(async () => {
      root = create(React.createElement(Probe, { host: 'example.test' }))
    })
    expect(mockState.request).toHaveBeenCalledTimes(1)
    expect(mockState.appStateListener).not.toBeNull()

    await act(async () => {
      mockState.appStateListener!('background')
    })
    // Three full tick windows pass — the loop schedules new ticks but every
    // tick is a no-op while `fgRef.current === false`.
    await act(async () => {
      jest.advanceTimersByTime(9_000)
    })
    expect(mockState.request).toHaveBeenCalledTimes(1)

    await act(async () => {
      root!.unmount()
    })
  })

  it('exposes the polled letters as state', async () => {
    mockState.request.mockResolvedValue(
      jsonResponse([{ bucket: '202604281230', letters: 'LRB' }])
    )
    let root: ReactTestRenderer
    await act(async () => {
      root = create(React.createElement(Probe, { host: 'example.test' }))
    })
    // Flush microtasks so the awaited json() resolves and setLive runs.
    await act(async () => {
      await Promise.resolve()
      await Promise.resolve()
    })
    expect(probeResult).not.toBeNull()
    expect(probeResult!.liveBucket).toBe('202604281230')
    expect(probeResult!.liveLetters).toBe('LRB')
    await act(async () => {
      root!.unmount()
    })
  })
})
