// Native-module shims for the integration tier.
//
// The integration tests load production code (`src/state/accountStore.ts`,
// `src/lib/api-context.ts`, the protocol adapters, …) in a plain Node
// environment via `jest-expo`. The production code imports a handful of
// native modules — `react-native-mmkv`, `expo-secure-store`,
// `expo-file-system`, `expo-crypto`, `react-native-watch-connectivity` —
// none of which load outside a real React-Native runtime. This file is the
// single place we tell jest how to substitute them.
//
// `setupFilesAfterEach` in `package.json` points every integration test
// file at this module, so the mocks are in place before any test imports
// `src/`. The mocks expose `__resetIntegrationMocks()` so individual tests
// can wipe in-process state between cases.

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

// Live-network tests pay roundtrip latency; the default 5s is too tight for
// `compose` (POST + GET + DELETE) and `auth` (verify + corrupt + retry).
jest.setTimeout(30_000)

// Note: `jest.mock` calls inside this module are hoisted to the top of the
// file by jest, exactly like in a test file — so the order of declarations
// here doesn't reflect runtime order. The `mock`-prefixed bindings below are
// the jest hoist-safe pattern documented in the AccountStore test.

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', () => ({
  WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: 'WHEN_PASSCODE_SET_THIS_DEVICE_ONLY',
  WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY',
  WHEN_UNLOCKED: 'WHEN_UNLOCKED',
  AFTER_FIRST_UNLOCK: 'AFTER_FIRST_UNLOCK',
  AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: 'AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY',
  ALWAYS: 'ALWAYS',
  ALWAYS_THIS_DEVICE_ONLY: 'ALWAYS_THIS_DEVICE_ONLY',
  getItem: (key: string) => mockSecure.getItem(key),
  setItem: (key: string, value: string) => mockSecure.setItem(key, value),
  deleteItemAsync: (key: string) => mockSecure.deleteItemAsync(key),
  // Async variants — some src/ callers use them.
  getItemAsync: async (key: string) => mockSecure.getItem(key),
  setItemAsync: async (key: string, value: string) => mockSecure.setItem(key, value),
}))

// `expo-file-system` is only consumed by AccountStore's dev-mirror path
// (`mirrorAccountsListToDisk`). The path is wrapped in try/catch so a
// missing native module is harmless, but the constructor noise pollutes
// test output — stub the API surface to no-ops.
jest.mock('expo-file-system', () => {
  class FakeDirectory {
    create(_opts?: unknown) {}
  }
  class FakeFile {
    public exists = false
    delete() {}
    create() {
      this.exists = true
    }
    write(_data: string) {}
  }
  return {
    Directory: FakeDirectory,
    File: FakeFile,
    Paths: { document: '/tmp/halflight-test-doc' },
  }
})

// `expo-crypto` is read by `randomKey` (Idempotency-Key generation). Node
// has webcrypto built-in; route through it so the live integration test
// path mints real random bytes, not deterministic counters.
jest.mock('expo-crypto', () => {
  const { webcrypto } = require('node:crypto') as { webcrypto: Crypto }
  return {
    getRandomBytesAsync: async (n: number) => {
      const out = new Uint8Array(n)
      webcrypto.getRandomValues(out)
      return out
    },
  }
})

// `expo-application` is read by branding's `userAgentHeader`. Return stable
// values so the user-agent string is deterministic in test logs.
jest.mock('expo-application', () => ({
  nativeApplicationVersion: '0.0.0-integration',
  nativeBuildVersion: '0',
  applicationName: 'Halflight (integration)',
}))

// Apple Watch handoff is iOS-runtime-only and entirely irrelevant to wire-
// level tests. Stub the whole module so AccountStore.setActive and
// api-context's refresh path are pure no-ops on the watch surface.
jest.mock('src/lib/watch/handoff', () => ({
  pushHandoffForAccount: () => {},
  pushActiveHandoff: () => {},
  revokeOnWatch: () => {},
  revokeAllOnWatch: () => {},
  enableWatchHandoff: () => {},
  subscribeToHandoffEvents: () => () => {},
  getWatchHandoffSnapshot: () => ({ accountId: null, lastHandoffAt: null }),
  getWatchStatus: async () => ({
    paired: false,
    appInstalled: false,
    reachable: false,
    accountId: null,
  }),
}))

// Drafts registry touches MMKV under the hood; the mock MMKV handles that,
// but its dev-mirror path also reaches into expo-file-system — already
// stubbed above. No additional mock needed.

// Suppress dev-only console.warn from `devnet/log` so test output is clean.
// Production paths set `__DEV__` to true via metro; we override to false for
// integration tests so the wire-level signal isn't drowned in [auth]/[net]/
// [push]/[wc] traces.
;(globalThis as { __DEV__?: boolean }).__DEV__ = false

/**
 * Reset every in-memory mock store. Call from `beforeEach` in any
 * integration test that needs a clean registry between cases.
 */
export function __resetIntegrationMocks(): void {
  mockMmkv.reset()
  mockSecure.reset()
}

export const __mockMmkv = mockMmkv
export const __mockSecure = mockSecure
