import { dataApiService } from '@data/DataApiService'
import { clearWebviewState, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
import type { MiniApp } from '@shared/data/types/miniApp'
import { MockDataApiUtils } from '@test-mocks/renderer/DataApiService'
import { MockUseCacheUtils } from '@test-mocks/renderer/useCache'
import { MockUseDataApi, MockUseDataApiUtils } from '@test-mocks/renderer/useDataApi'
import { MockUsePreferenceUtils } from '@test-mocks/renderer/usePreference'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const mockTabs = vi.hoisted(() => ({
  tabs: [] as Array<{ id: string; url: string }>,
  hasContext: true,
  closeTab: vi.fn(),
  updateTab: vi.fn()
}))

vi.mock('@renderer/hooks/tab', () => ({
  useOptionalTabsContext: () =>
    mockTabs.hasContext
      ? {
          tabs: mockTabs.tabs,
          closeTab: mockTabs.closeTab,
          updateTab: mockTabs.updateTab
        }
      : null
}))

vi.mock('@renderer/utils/webviewStateManager', () => ({
  clearWebviewState: vi.fn(),
  setWebviewLoaded: vi.fn()
}))

import { __resetRegionDetectionForTesting, useMiniApps } from '../useMiniApps'
import { appFixtures, createCnOnlyApp, createGlobalApp, createMiniApp } from './fixtures/miniApp'

/** Helper: return the array directly since list() now returns a bare MiniApp[] */
const paginated = (items: MiniApp[]) => items
const mockClearWebviewState = vi.mocked(clearWebviewState)
const mockSetWebviewLoaded = vi.mocked(setWebviewLoaded)

describe('useMiniApps', () => {
  beforeEach(() => {
    MockUseCacheUtils.resetMocks()
    MockUsePreferenceUtils.resetMocks()
    MockUseDataApiUtils.resetMocks()
    MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))

    // Reset module-level regionDetectionPromise to ensure fresh detection in each test
    __resetRegionDetectionForTesting()
    mockTabs.tabs = []
    mockTabs.hasContext = true
    mockTabs.closeTab.mockClear()
    mockTabs.updateTab.mockClear()
    mockClearWebviewState.mockClear()
    mockSetWebviewLoaded.mockClear()
  })

  // === Data Loading ===

  describe('data loading', () => {
    it('should return empty arrays when no data', () => {
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.allApps).toEqual([])
      expect(result.current.miniApps).toEqual([])
      expect(result.current.disabled).toEqual([])
      expect(result.current.pinned).toEqual([])
    })

    it('should return all apps merged with presets', () => {
      const apps = [
        appFixtures.mixedStatus.enabled1,
        appFixtures.mixedStatus.disabled1,
        appFixtures.mixedStatus.pinned1
      ]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.allApps).toHaveLength(3)
      expect(result.current.allApps.map((a: MiniApp) => a.appId)).toEqual(['enabled1', 'disabled1', 'pinned1'])
    })

    it('should split apps by status correctly', () => {
      const { mixedStatus } = appFixtures
      const apps = [mixedStatus.enabled1, mixedStatus.enabled2, mixedStatus.disabled1, mixedStatus.pinned1]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      const { result } = renderHook(() => useMiniApps())
      // miniApps includes enabled + pinned apps (pinned apps remain visible in the grid)
      expect(result.current.miniApps).toHaveLength(3)
      expect(result.current.disabled).toHaveLength(1)
      expect(result.current.pinned).toHaveLength(1)
    })

    it('should expose isLoading state', () => {
      MockUseDataApiUtils.mockQueryLoading('/mini-apps')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.isLoading).toBe(true)
    })

    it('should expose refetch function', () => {
      const { result } = renderHook(() => useMiniApps())
      expect(typeof result.current.refetch).toBe('function')
    })
  })

  // === Region Filtering ===

  describe('region filtering', () => {
    it('should show all apps when region is CN (default)', () => {
      const { mixedRegion } = appFixtures
      const apps = Object.values(mixedRegion).map((a) => ({ ...a, status: 'enabled' as const }))
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'CN')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(3)
    })

    it('should only show Global apps when region is Global', () => {
      const { mixedRegion } = appFixtures
      const apps = Object.values(mixedRegion).map((a) => ({ ...a, status: 'enabled' as const }))
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(1)
      expect(result.current.miniApps[0].appId).toBe('global-app')
    })

    it('should show apps without supportedRegions as CN-only (hidden from Global)', () => {
      const { mixedRegion } = appFixtures
      const apps = [mixedRegion.globalApp, mixedRegion.noRegionApp].map((a) => ({
        ...a,
        status: 'enabled' as const
      }))
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(1)
      expect(result.current.miniApps[0].appId).toBe('global-app')
    })

    it('should not filter pinned apps by region', () => {
      const apps = [
        createGlobalApp('g-pinned', { status: 'pinned' }),
        createCnOnlyApp('cn-pinned', { status: 'pinned' }),
        createMiniApp('nr-pinned', { status: 'pinned' })
      ]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.pinned).toHaveLength(3)
    })

    it('should filter disabled apps by region', () => {
      const apps = [
        createGlobalApp('global-disabled', { status: 'disabled' }),
        createCnOnlyApp('cn-disabled', { status: 'disabled' })
      ]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.disabled).toHaveLength(1)
      expect(result.current.disabled[0].appId).toBe('global-disabled')
    })
  })

  // === Effective Region Calculation ===

  describe('effective region calculation', () => {
    it('should use preference CN when explicitly set', () => {
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'CN')
      const apps = [createGlobalApp('g', { status: 'enabled' }), createCnOnlyApp('c', { status: 'enabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(2)
    })

    it('should use preference Global when explicitly set', () => {
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const apps = [createGlobalApp('g', { status: 'enabled' }), createCnOnlyApp('c', { status: 'enabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(1)
      expect(result.current.miniApps[0].appId).toBe('g')
    })

    it('should use detected region when preference is auto and detected region exists', () => {
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'auto')
      MockUseCacheUtils.setCacheValue('mini_app.detected_region', 'Global')
      const apps = [createGlobalApp('g', { status: 'enabled' }), createCnOnlyApp('c', { status: 'enabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(1)
    })

    it('should default to CN when preference is auto and no detected region', () => {
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'auto')
      MockUseCacheUtils.setCacheValue('mini_app.detected_region', null)
      const apps = [createGlobalApp('g', { status: 'enabled' }), createCnOnlyApp('c', { status: 'enabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(2)
    })
  })

  // === UI State Cache ===

  describe('UI state cache', () => {
    it('should expose openedKeepAliveMiniApps from cache', () => {
      const keepAliveApps = [createMiniApp('app1'), createMiniApp('app2')]
      MockUseCacheUtils.setCacheValue('mini_app.opened_keep_alive', keepAliveApps)
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.openedKeepAliveMiniApps).toEqual(keepAliveApps)
    })

    it('should expose currentMiniAppId from cache', () => {
      MockUseCacheUtils.setCacheValue('mini_app.current_id', 'my-app')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.currentMiniAppId).toBe('my-app')
    })

    it('should expose miniAppShow from cache', () => {
      MockUseCacheUtils.setCacheValue('mini_app.show', true)
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniAppShow).toBe(true)
    })

    it('should expose openedOneOffMiniApp from cache', () => {
      const oneOffApp = createMiniApp('one-off')
      MockUseCacheUtils.setCacheValue('mini_app.opened_oneoff', oneOffApp)
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.openedOneOffMiniApp).toEqual(oneOffApp)
    })

    it('should expose setters for UI state', () => {
      const { result } = renderHook(() => useMiniApps())
      expect(typeof result.current.setOpenedKeepAliveMiniApps).toBe('function')
      expect(typeof result.current.setCurrentMiniAppId).toBe('function')
      expect(typeof result.current.setMiniAppShow).toBe('function')
      expect(typeof result.current.setOpenedOneOffMiniApp).toBe('function')
    })

    it('should update openedKeepAliveMiniApps when setter is called', async () => {
      const { result } = renderHook(() => useMiniApps())
      const newApps = [createMiniApp('new-app')]
      await act(async () => {
        result.current.setOpenedKeepAliveMiniApps(newApps)
      })
      // Check cache values directly since mock useCache doesn't trigger re-renders
      expect(MockUseCacheUtils.getCacheValue('mini_app.opened_keep_alive')).toEqual(newApps)
    })
  })

  // === Mutations ===

  describe('mutations', () => {
    it('should expose all mutation functions', () => {
      const { result } = renderHook(() => useMiniApps())
      expect(typeof result.current.updateAppStatus).toBe('function')
      expect(typeof result.current.setAppStatusBulk).toBe('function')
      expect(typeof result.current.createCustomMiniApp).toBe('function')
      expect(typeof result.current.removeCustomMiniApp).toBe('function')
      expect(typeof result.current.reorderMiniApps).toBe('function')
    })

    it('should sync opened cache, tab metadata, and webview state after updating a custom miniapp', async () => {
      const existing = createMiniApp('custom-app', {
        name: 'Old App',
        url: 'https://old.example.com',
        logo: 'old-logo',
        presetMiniAppId: null
      })
      const other = createMiniApp('other-app')
      const updated = {
        ...existing,
        name: 'New App',
        url: 'https://new.example.com',
        logo: 'new-logo'
      }
      const trigger = vi.fn().mockResolvedValue(updated)
      MockUseDataApiUtils.mockMutationWithTrigger('PATCH', '/mini-apps/:appId', trigger)
      MockUseCacheUtils.setCacheValue('mini_app.opened_keep_alive', [other, existing])
      MockUseCacheUtils.setCacheValue('mini_app.opened_oneoff', existing)
      mockTabs.tabs = [
        { id: 'tab-1', url: '/app/mini-app/custom-app' },
        { id: 'tab-2', url: '/app/mini-app/custom-app-extra' }
      ]

      const { result } = renderHook(() => useMiniApps())

      await act(async () => {
        await result.current.updateCustomMiniApp('custom-app', {
          name: 'New App',
          url: 'https://new.example.com',
          logo: 'new-logo'
        })
      })

      expect(trigger).toHaveBeenCalledWith({
        params: { appId: 'custom-app' },
        body: {
          name: 'New App',
          url: 'https://new.example.com',
          logo: 'new-logo'
        }
      })
      expect(MockUseCacheUtils.getCacheValue('mini_app.opened_keep_alive')).toEqual([other, updated])
      expect(MockUseCacheUtils.getCacheValue('mini_app.opened_oneoff')).toEqual(updated)
      expect(mockSetWebviewLoaded).toHaveBeenCalledWith('custom-app', false)
      expect(mockTabs.updateTab).toHaveBeenCalledWith('tab-1', { title: 'New App', icon: 'new-logo' })
      expect(mockTabs.updateTab).not.toHaveBeenCalledWith('tab-2', expect.anything())
    })

    it('should clean opened cache, tabs, and webview state after removing a custom miniapp', async () => {
      const existing = createMiniApp('custom-app', { presetMiniAppId: null })
      const other = createMiniApp('other-app')
      const trigger = vi.fn().mockResolvedValue(undefined)
      MockUseDataApiUtils.mockMutationWithTrigger('DELETE', '/mini-apps/:appId', trigger)
      MockUseCacheUtils.setCacheValue('mini_app.opened_keep_alive', [existing, other])
      MockUseCacheUtils.setCacheValue('mini_app.opened_oneoff', existing)
      MockUseCacheUtils.setCacheValue('mini_app.current_id', 'custom-app')
      MockUseCacheUtils.setCacheValue('mini_app.show', true)
      mockTabs.tabs = [
        { id: 'tab-1', url: '/app/mini-app/custom-app' },
        { id: 'tab-2', url: '/app/mini-app/custom-app-extra' }
      ]

      const { result } = renderHook(() => useMiniApps())

      await act(async () => {
        await result.current.removeCustomMiniApp('custom-app')
      })

      expect(trigger).toHaveBeenCalledWith({ params: { appId: 'custom-app' } })
      expect(MockUseCacheUtils.getCacheValue('mini_app.opened_keep_alive')).toEqual([other])
      expect(MockUseCacheUtils.getCacheValue('mini_app.opened_oneoff')).toBeNull()
      expect(MockUseCacheUtils.getCacheValue('mini_app.current_id')).toBe('')
      expect(MockUseCacheUtils.getCacheValue('mini_app.show')).toBe(false)
      expect(mockClearWebviewState).toHaveBeenCalledWith('custom-app')
      expect(mockTabs.closeTab).toHaveBeenCalledWith('tab-1')
      expect(mockTabs.closeTab).not.toHaveBeenCalledWith('tab-2')
    })
  })

  // === setAppStatusBulk ===

  describe('setAppStatusBulk', () => {
    it('issues exactly one PATCH per requested update', async () => {
      const apps = [createMiniApp('a', { status: 'enabled' }), createMiniApp('b', { status: 'disabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      const { result } = renderHook(() => useMiniApps())
      MockDataApiUtils.resetMocks()

      await act(async () => {
        await result.current.setAppStatusBulk([
          { appId: 'a', status: 'disabled' },
          { appId: 'b', status: 'enabled' }
        ])
      })

      const patchCalls = MockDataApiUtils.getCalls('patch')
      expect(patchCalls).toContainEqual(['/mini-apps/a', { body: { status: 'disabled' } }])
      expect(patchCalls).toContainEqual(['/mini-apps/b', { body: { status: 'enabled' } }])
      expect(patchCalls).toHaveLength(2)
    })

    it('does not touch rows the caller never names — region-hidden apps stay put', async () => {
      // Replaces the legacy "updateMiniApps under Global mode disables CN apps"
      // bug. With the command-style API the caller only PATCHes what it names.
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const globalApp = createGlobalApp('globalA', { status: 'enabled' })
      const cnOnly = createCnOnlyApp('cnOnly', { status: 'enabled' })
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([globalApp, cnOnly]))

      const { result } = renderHook(() => useMiniApps())
      MockDataApiUtils.resetMocks()

      // Hide the only visible Global app — should produce one PATCH for it,
      // never sweep the region-hidden CN app into disabled.
      await act(async () => {
        await result.current.setAppStatusBulk([{ appId: 'globalA', status: 'disabled' }])
      })

      const patchCalls = MockDataApiUtils.getCalls('patch')
      expect(patchCalls).toContainEqual(['/mini-apps/globalA', { body: { status: 'disabled' } }])
      expect(patchCalls.find(([path]) => path === '/mini-apps/cnOnly')).toBeUndefined()
    })

    it('returns immediately for an empty update list (no PATCH calls)', async () => {
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))
      const { result } = renderHook(() => useMiniApps())
      MockDataApiUtils.resetMocks()

      await act(async () => {
        await result.current.setAppStatusBulk([])
      })

      expect(MockDataApiUtils.getCalls('patch')).toHaveLength(0)
    })
  })

  // === updateAppStatus ===

  describe('updateAppStatus', () => {
    it('should call the patch mutation trigger with the new status', async () => {
      const mockTrigger = vi.fn().mockResolvedValue({ success: true })
      const { mockUseMutation } = await import('@test-mocks/renderer/useDataApi')
      mockUseMutation.mockImplementation((method: string, path: string) => {
        if (method === 'PATCH' && path === '/mini-apps/:appId') {
          return { trigger: mockTrigger, isLoading: false, error: undefined }
        }
        return { trigger: vi.fn().mockResolvedValue({ success: true }), isLoading: false, error: undefined }
      })

      const apps = [createMiniApp('app1', { status: 'enabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      const { result } = renderHook(() => useMiniApps())

      await act(async () => {
        await result.current.updateAppStatus('app1', 'disabled')
      })

      expect(mockTrigger).toHaveBeenCalledWith({ params: { appId: 'app1' }, body: { status: 'disabled' } })
    })
  })

  // === reorderMiniApps ===
  /**
   * NOTE: `sortOrder` changes MUST use the `reorderMiniApps` mutation (PATCH /mini-apps),
   * not individual `updateAppStatus` or `patchApp` calls. The reorder endpoint accepts
   * an ordered list of { appId, sortOrder } items and atomically updates all positions.
   * Directly mutating `sortOrder` via individual PATCH calls can cause race conditions
   * and inconsistent ordering.
   */

  describe('reorderMiniApps', () => {
    it('should expose a callable reorder function backed by useReorder', async () => {
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))
      const { result } = renderHook(() => useMiniApps())
      // The actual ordering logic is tested in useReorder; here we just verify wiring.
      expect(typeof result.current.reorderMiniApps).toBe('function')
      expect(typeof result.current.reorderMiniAppsByStatus).toBe('function')
    })

    it('should reorder visible apps against the displayed subset orderKey baseline', async () => {
      const patchOrderTrigger = vi.fn().mockResolvedValue(undefined)
      const patchBatchTrigger = vi.fn().mockResolvedValue(undefined)
      MockUseDataApi.useMutation.mockImplementation((method, path) => {
        if (method === 'PATCH' && path === '/mini-apps/:id/order') {
          return { trigger: patchOrderTrigger, isLoading: false, error: undefined }
        }
        if (method === 'PATCH' && path === '/mini-apps/order:batch') {
          return { trigger: patchBatchTrigger, isLoading: false, error: undefined }
        }
        return { trigger: vi.fn().mockResolvedValue({ success: true }), isLoading: false, error: undefined }
      })

      const enabled = createGlobalApp('enabled', { status: 'enabled', orderKey: 'a0' })
      const regionHidden = createCnOnlyApp('region-hidden', { status: 'enabled', orderKey: 'a1' })
      const pinned = createGlobalApp('pinned', { status: 'pinned', orderKey: 'b0' })
      const hidden = createMiniApp('hidden', { status: 'disabled', orderKey: 'a0' })
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([pinned, enabled, regionHidden, hidden]))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')

      const { result } = renderHook(() => useMiniApps())

      expect(result.current.miniApps.map((app) => app.appId)).toEqual(['enabled', 'pinned'])

      await act(async () => {
        await result.current.reorderMiniAppsByStatus('visible', [pinned, enabled])
      })

      expect(patchOrderTrigger).toHaveBeenCalledWith({ params: { id: 'pinned' }, body: { position: 'first' } })
      expect(patchBatchTrigger).not.toHaveBeenCalled()
    })
  })

  // === Edge Cases ===

  describe('edge cases', () => {
    it('should handle empty enabled list gracefully', () => {
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toEqual([])
    })

    it('should handle preset apps with empty supportedRegions array as CN-only', () => {
      const apps = [createMiniApp('empty-regions', { supportedRegions: [], status: 'enabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(0)
    })

    it('should treat custom apps without supportedRegions as visible everywhere', () => {
      // Custom rows (presetMiniAppId === null) without region info come from
      // migrated v1 data or hand-added apps. Defaulting them to CN-only would
      // hide a user's own app under Global.
      const apps = [createMiniApp('mine', { presetMiniAppId: null, status: 'enabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      const { result } = renderHook(() => useMiniApps())
      expect(result.current.miniApps).toHaveLength(1)
    })

    it('should return consistent shape across renders', () => {
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([createMiniApp('app1')]))
      const { result, rerender } = renderHook(() => useMiniApps())
      const firstShape = Object.keys(result.current).sort()
      rerender()
      const secondShape = Object.keys(result.current).sort()
      expect(firstShape).toEqual(secondShape)
    })
  })

  // === Region Auto-Detection ===

  describe('region auto-detection', () => {
    beforeEach(() => {
      // Reset the module-level promise between tests
      // We need to re-import the module or access the internal state
      // Since regionDetectionPromise is module-scoped, we test via the hook's useEffect
    })

    it('should call setDetectedRegion with CN when IP resolves to CN', async () => {
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'auto')
      MockUseCacheUtils.setCacheValue('mini_app.detected_region', null)
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))

      // Mock window.api.getIpCountry to resolve 'CN'
      const originalGetIpCountry = window.api?.getIpCountry
      Object.defineProperty(window, 'api', {
        value: { getIpCountry: vi.fn().mockResolvedValue('CN') },
        writable: true,
        configurable: true
      })

      renderHook(() => useMiniApps())

      // Wait for the async detection to complete
      await act(async () => {
        await new Promise((resolve) => setTimeout(resolve, 50))
      })

      expect(MockUseCacheUtils.getCacheValue('mini_app.detected_region')).toBe('CN')

      // Restore
      if (originalGetIpCountry) {
        Object.defineProperty(window, 'api', {
          value: { getIpCountry: originalGetIpCountry },
          writable: true,
          configurable: true
        })
      }
    })

    it('should call setDetectedRegion with Global when IP resolves to US', async () => {
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'auto')
      MockUseCacheUtils.setCacheValue('mini_app.detected_region', null)
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))

      const originalGetIpCountry = window.api?.getIpCountry
      Object.defineProperty(window, 'api', {
        value: { getIpCountry: vi.fn().mockResolvedValue('US') },
        writable: true,
        configurable: true
      })

      renderHook(() => useMiniApps())

      await act(async () => {
        await new Promise((resolve) => setTimeout(resolve, 50))
      })

      expect(MockUseCacheUtils.getCacheValue('mini_app.detected_region')).toBe('Global')

      if (originalGetIpCountry) {
        Object.defineProperty(window, 'api', {
          value: { getIpCountry: originalGetIpCountry },
          writable: true,
          configurable: true
        })
      }
    })

    it('should fallback to CN when IP detection rejects', async () => {
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'auto')
      MockUseCacheUtils.setCacheValue('mini_app.detected_region', null)
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))

      const originalGetIpCountry = window.api?.getIpCountry
      Object.defineProperty(window, 'api', {
        value: { getIpCountry: vi.fn().mockRejectedValue(new Error('Network error')) },
        writable: true,
        configurable: true
      })

      renderHook(() => useMiniApps())

      await act(async () => {
        await new Promise((resolve) => setTimeout(resolve, 50))
      })

      expect(MockUseCacheUtils.getCacheValue('mini_app.detected_region')).toBe('CN')

      if (originalGetIpCountry) {
        Object.defineProperty(window, 'api', {
          value: { getIpCountry: originalGetIpCountry },
          writable: true,
          configurable: true
        })
      }
    })

    it('should not call detectUserRegion when region is explicitly set', async () => {
      MockUsePreferenceUtils.setPreferenceValue('feature.mini_app.region', 'Global')
      MockUseCacheUtils.setCacheValue('mini_app.detected_region', null)
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated([]))

      const getIpCountryMock = vi.fn().mockResolvedValue('US')
      const originalGetIpCountry = window.api?.getIpCountry
      Object.defineProperty(window, 'api', {
        value: { getIpCountry: getIpCountryMock },
        writable: true,
        configurable: true
      })

      renderHook(() => useMiniApps())

      await act(async () => {
        await new Promise((resolve) => setTimeout(resolve, 50))
      })

      // IP detection should not be called when region is explicitly set
      expect(getIpCountryMock).not.toHaveBeenCalled()

      if (originalGetIpCountry) {
        Object.defineProperty(window, 'api', {
          value: { getIpCountry: originalGetIpCountry },
          writable: true,
          configurable: true
        })
      }
    })
  })

  // === setAppStatusBulk partial-failure ===

  describe('setAppStatusBulk partial-failure', () => {
    it('throws when one of the PATCHes fails and invalidates the cache', async () => {
      const apps = [createMiniApp('app1', { status: 'disabled' }), createMiniApp('app2', { status: 'disabled' })]
      MockUseDataApiUtils.mockQueryData('/mini-apps', paginated(apps))

      vi.mocked(dataApiService.patch).mockImplementation(async (path: string) => {
        if (path === '/mini-apps/app1') return { success: true } as never
        if (path === '/mini-apps/app2') throw new Error('Server error')
        return undefined as never
      })

      const { result } = renderHook(() => useMiniApps())

      await act(async () => {
        await expect(
          result.current.setAppStatusBulk([
            { appId: 'app1', status: 'enabled' },
            { appId: 'app2', status: 'enabled' }
          ])
        ).rejects.toThrow()
      })
    })
  })
})
