// P3b/P3c/P3e — media cache configuration helpers.
//
// Boot calls `applyVideoCacheCap()` and `applyImageCacheCap()` once so the
// platform limits match the user's stored preference (or our defaults).
// `setVideoCacheCap` / `setImageCacheCap` persist the new limit AND reapply
// it. `maybeAutoEvict` honours the user's auto-clear cadence and only fires
// when the configured interval has elapsed since the last fire.
//
// Pins:
//   1. Defaults: 256 MB video, 512 MB image.
//   2. Setters persist + apply.
//   3. Image cap is iOS-only (Android skips the configureCache call).
//   4. Auto-evict: 'never' is a no-op; 'weekly' fires after >= 7d gap;
//      cleared timestamp is recorded so the next launch isn't a re-fire.

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

const mockMmkv = createMmkvInstances()
const mockSecure = createSecureStoreMock()
const mockSetVideoCacheSizeAsync = jest.fn<Promise<void>, [number]>(async () => undefined)
const mockClearVideoCacheAsync = jest.fn<Promise<void>, []>(async () => undefined)
const mockGetCurrentVideoCacheSize = jest.fn<number, []>(() => 0)
const mockClearDiskCache = jest.fn<Promise<boolean>, []>(async () => true)
const mockClearMemoryCache = jest.fn<Promise<boolean>, []>(async () => true)
const mockConfigureCache = jest.fn<void, [{ maxDiskSize?: number }]>()

let mockOS: 'ios' | 'android' = 'ios'

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),
  WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: 0,
}))

jest.mock('expo-video', () => ({
  setVideoCacheSizeAsync: (n: number) => mockSetVideoCacheSizeAsync(n),
  clearVideoCacheAsync: () => mockClearVideoCacheAsync(),
  getCurrentVideoCacheSize: () => mockGetCurrentVideoCacheSize(),
}))

jest.mock('expo-image', () => ({
  Image: {
    clearDiskCache: () => mockClearDiskCache(),
    clearMemoryCache: () => mockClearMemoryCache(),
    configureCache: (cfg: { maxDiskSize?: number }) => mockConfigureCache(cfg),
  },
}))

jest.mock('react-native', () => ({
  Platform: {
    get OS() {
      return mockOS
    },
  },
}))

type CacheConfigModule = typeof import('../src/lib/media/cacheConfig')

let cacheConfig: CacheConfigModule

beforeAll(() => {
  cacheConfig = require('../src/lib/media/cacheConfig') as CacheConfigModule
})

beforeEach(() => {
  mockMmkv.reset()
  mockSecure.reset()
  mockSetVideoCacheSizeAsync.mockClear()
  mockClearVideoCacheAsync.mockClear()
  mockGetCurrentVideoCacheSize.mockReset().mockReturnValue(0)
  mockClearDiskCache.mockClear()
  mockClearMemoryCache.mockClear()
  mockConfigureCache.mockClear()
  mockOS = 'ios'
})

describe('cacheConfig — defaults and getters', () => {
  it('video cap defaults to 256 MB when unset', () => {
    expect(cacheConfig.getVideoCacheCap()).toBe(256 * 1024 * 1024)
  })

  it('image cap defaults to 512 MB when unset', () => {
    expect(cacheConfig.getImageCacheCap()).toBe(512 * 1024 * 1024)
  })

  it('auto-evict policy defaults to never when unset', () => {
    expect(cacheConfig.getAutoEvictPolicy()).toBe('never')
  })

  it('coerces unknown stored policies back to never', () => {
    const app = mockMmkv.getOrCreate('app')
    app.set('media.autoEvictPolicy', 'lol-not-a-policy')
    expect(cacheConfig.getAutoEvictPolicy()).toBe('never')
  })
})

describe('cacheConfig — setters persist and apply', () => {
  it('setVideoCacheCap persists to MMKV and calls native', async () => {
    await cacheConfig.setVideoCacheCap(128 * 1024 * 1024)
    expect(cacheConfig.getVideoCacheCap()).toBe(128 * 1024 * 1024)
    expect(mockSetVideoCacheSizeAsync).toHaveBeenCalledWith(128 * 1024 * 1024)
  })

  it('setImageCacheCap calls configureCache on iOS', () => {
    mockOS = 'ios'
    cacheConfig.setImageCacheCap(1024 * 1024 * 1024)
    expect(cacheConfig.getImageCacheCap()).toBe(1024 * 1024 * 1024)
    expect(mockConfigureCache).toHaveBeenCalledWith({
      maxDiskSize: 1024 * 1024 * 1024,
    })
  })

  it('setImageCacheCap skips configureCache on Android (no programmatic API)', () => {
    mockOS = 'android'
    cacheConfig.setImageCacheCap(1024 * 1024 * 1024)
    expect(cacheConfig.getImageCacheCap()).toBe(1024 * 1024 * 1024)
    expect(mockConfigureCache).not.toHaveBeenCalled()
  })

  it('applyVideoCacheCap reads stored value and pushes to native', async () => {
    const app = mockMmkv.getOrCreate('app')
    app.set('media.videoCacheBytes', 64 * 1024 * 1024)
    await cacheConfig.applyVideoCacheCap()
    expect(mockSetVideoCacheSizeAsync).toHaveBeenCalledWith(64 * 1024 * 1024)
  })

  it('applyImageCacheCap is a no-op on Android', () => {
    mockOS = 'android'
    cacheConfig.applyImageCacheCap()
    expect(mockConfigureCache).not.toHaveBeenCalled()
  })
})

describe('cacheConfig — maybeAutoEvict', () => {
  it("never policy doesn't fire", async () => {
    cacheConfig.setAutoEvictPolicy('never')
    await cacheConfig.maybeAutoEvict()
    expect(mockClearDiskCache).not.toHaveBeenCalled()
    expect(mockClearVideoCacheAsync).not.toHaveBeenCalled()
  })

  it('weekly policy fires when last eviction was > 7 days ago', async () => {
    cacheConfig.setAutoEvictPolicy('weekly')
    const eightDaysAgo = Date.now() - 8 * 24 * 60 * 60 * 1000
    mockMmkv.getOrCreate('app').set('media.lastEvictionAt', eightDaysAgo)
    await cacheConfig.maybeAutoEvict()
    expect(mockClearDiskCache).toHaveBeenCalledTimes(1)
    expect(mockClearVideoCacheAsync).toHaveBeenCalledTimes(1)
    // Records the new fire timestamp so a re-call is a no-op.
    const last = mockMmkv.getOrCreate('app').getNumber('media.lastEvictionAt') ?? 0
    expect(last).toBeGreaterThan(eightDaysAgo)
  })

  it('weekly policy stays inert when last eviction was < 7 days ago', async () => {
    cacheConfig.setAutoEvictPolicy('weekly')
    const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
    mockMmkv.getOrCreate('app').set('media.lastEvictionAt', oneDayAgo)
    await cacheConfig.maybeAutoEvict()
    expect(mockClearDiskCache).not.toHaveBeenCalled()
    expect(mockClearVideoCacheAsync).not.toHaveBeenCalled()
  })

  it('monthly policy fires after >= 30 days', async () => {
    cacheConfig.setAutoEvictPolicy('monthly')
    const thirtyOneDaysAgo = Date.now() - 31 * 24 * 60 * 60 * 1000
    mockMmkv.getOrCreate('app').set('media.lastEvictionAt', thirtyOneDaysAgo)
    await cacheConfig.maybeAutoEvict()
    expect(mockClearVideoCacheAsync).toHaveBeenCalledTimes(1)
  })
})

describe('cacheConfig — clearAllMediaCaches', () => {
  it('clears every cache and reapplies caps', async () => {
    await cacheConfig.clearAllMediaCaches()
    expect(mockClearDiskCache).toHaveBeenCalledTimes(1)
    expect(mockClearMemoryCache).toHaveBeenCalledTimes(1)
    expect(mockClearVideoCacheAsync).toHaveBeenCalledTimes(1)
    // Re-applies the video cap (and image cap on iOS).
    expect(mockSetVideoCacheSizeAsync).toHaveBeenCalled()
  })

  it('records the eviction timestamp', async () => {
    await cacheConfig.clearAllMediaCaches()
    const last = mockMmkv.getOrCreate('app').getNumber('media.lastEvictionAt')
    expect(last).toBeGreaterThan(0)
  })
})

describe('cacheConfig — getCurrentVideoCacheBytes', () => {
  it('reads from expo-video', () => {
    mockGetCurrentVideoCacheSize.mockReturnValue(12345)
    expect(cacheConfig.getCurrentVideoCacheBytes()).toBe(12345)
  })

  it('returns 0 if expo-video throws', () => {
    mockGetCurrentVideoCacheSize.mockImplementation(() => {
      throw new Error('not ready')
    })
    expect(cacheConfig.getCurrentVideoCacheBytes()).toBe(0)
  })
})
