// AccountStore behavior lock-down. Covers the surfaces every later refactor of
// `src/state/accountStore.ts` will mutate: snapshot atomicity, the self-heal
// path against corrupted/mismatched profile blobs, switch ordering, the
// 8-account cap, fallback-id selection, instance+userId lookup, registry
// teardown on remove. Also includes a targeted regression test for the
// pre-Phase-2.1 cross-account write that bit the user three times.
//
// Built on the reusable helpers in `__tests__/_helpers/` so future store tests
// (notifications/marker.ts, future per-account stores) can import the same
// MMKV/SecureStore shims rather than reinventing them inline.

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

// `mock`-prefixed bindings: jest.mock factories are hoisted above all imports
// but cannot reference out-of-scope variables unless they begin with `mock`.
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', () => ({
  getItem: (key: string) => mockSecure.getItem(key),
  setItem: (key: string, value: string) => mockSecure.setItem(key, value),
  deleteItemAsync: (key: string) => mockSecure.deleteItemAsync(key),
}))

type StoreModule = typeof import('../src/state/accountStore')

// `accountStore.ts` initializes module-level snapshots at load. Each test
// needs a fresh module state so prior-test list/active-id leftovers don't
// contaminate the in-memory snapshot caches. `jest.isolateModules` is the
// proven shape (see `tapestry-emit.ts:90`).
function loadStore(): StoreModule {
  let mod: StoreModule | null = null
  jest.isolateModules(() => {
    mod = require('../src/state/accountStore') as StoreModule
  })
  return mod!
}

type UpsertInput = Parameters<StoreModule['AccountStore']['upsert']>[0]

const baseUpsert = (overrides: Partial<UpsertInput> = {}): UpsertInput => ({
  type: 'pixelfed',
  instance: 'example.test',
  userId: 'u_default',
  username: 'default',
  acct: 'default@example.test',
  displayName: 'Default',
  avatarUrl: null,
  ...overrides,
})

beforeEach(() => {
  mockMmkv.reset()
  mockSecure.reset()
})

describe('AccountStore — snapshot atomicity', () => {
  it('activeId, activeAccount, activeUserCache read consistently after setActive', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setProfileBlob(a.accountId, JSON.stringify({ id: 'u_a', username: 'A' }))
    AccountStore.setProfileBlob(b.accountId, JSON.stringify({ id: 'u_b', username: 'B' }))

    AccountStore.setActive(a.accountId)
    expect(AccountStore.activeId()).toBe(a.accountId)
    expect(AccountStore.activeAccount()?.accountId).toBe(a.accountId)
    expect(AccountStore.activeUserCache()?.id).toBe('u_a')

    AccountStore.setActive(b.accountId)
    expect(AccountStore.activeId()).toBe(b.accountId)
    expect(AccountStore.activeAccount()?.accountId).toBe(b.accountId)
    expect(AccountStore.activeUserCache()?.id).toBe('u_b')
  })

  it('subscribers always observe consistent (id, account, userCache) tuples', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setProfileBlob(a.accountId, JSON.stringify({ id: 'u_a' }))
    AccountStore.setProfileBlob(b.accountId, JSON.stringify({ id: 'u_b' }))
    AccountStore.setActive(a.accountId)

    const observed: Array<{
      id: string | null
      acctId: string | null
      cacheId: string | null
    }> = []
    AccountStore.subscribe(() => {
      observed.push({
        id: AccountStore.activeId(),
        acctId: AccountStore.activeAccount()?.accountId ?? null,
        cacheId: AccountStore.activeUserCache()?.id ?? null,
      })
    })

    AccountStore.setActive(b.accountId)
    // Every observation must point at the same account — no
    // (newId, oldUserCache) intermediate.
    for (const tick of observed) {
      if (tick.id !== null) {
        expect(tick.acctId).toBe(tick.id)
      }
    }
    const last = observed.at(-1)
    expect(last?.id).toBe(b.accountId)
    expect(last?.acctId).toBe(b.accountId)
    expect(last?.cacheId).toBe('u_b')
  })
})

describe('AccountStore — profile blob self-heal', () => {
  it('blob with matching id is returned unchanged', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.setActive(a.accountId)
    const blob = { id: 'u_a', username: 'alice', is_admin: true }
    AccountStore.setProfileBlob(a.accountId, JSON.stringify(blob))
    expect(AccountStore.activeUserCache()).toMatchObject(blob)
  })

  it('mismatched blob.id wipes the slot and returns null', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.setActive(a.accountId)
    AccountStore.setProfileBlob(
      a.accountId,
      JSON.stringify({ id: 'u_other', username: 'X' })
    )
    expect(AccountStore.activeUserCache()).toBeNull()
    const userMmkv = mockMmkv.get('user')
    expect(userMmkv?.getString(`user.account.${a.accountId}.profile`)).toBeUndefined()
  })

  it('corrupt JSON wipes the slot and returns null', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.setActive(a.accountId)
    AccountStore.setProfileBlob(a.accountId, 'not json')
    expect(AccountStore.activeUserCache()).toBeNull()
    const userMmkv = mockMmkv.get('user')
    expect(userMmkv?.getString(`user.account.${a.accountId}.profile`)).toBeUndefined()
  })

  it('regression — pre-Phase-2.1 cross-account contamination self-heals', () => {
    // The bug: a switch race wrote A's verify_credentials response into B's
    // slot. With B active, every `useUserCache()` reader saw A's identity.
    // Self-heal must wipe and return null so cold-start verify rebuilds B.
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setActive(b.accountId)
    AccountStore.setProfileBlob(
      b.accountId,
      JSON.stringify({ id: 'u_a', username: 'alice' })
    )
    expect(AccountStore.activeUserCache()).toBeNull()
    const userMmkv = mockMmkv.get('user')
    expect(userMmkv?.getString(`user.account.${b.accountId}.profile`)).toBeUndefined()
    // A's own slot is untouched — the self-heal targets only the active
    // slot whose blob mismatches the active summary's userId.
    expect(userMmkv?.getString(`user.account.${a.accountId}.profile`)).toBeUndefined()
  })
})

describe('AccountStore — switch ordering', () => {
  it('userCache is null on switch to an account with no blob, refills on write', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setProfileBlob(a.accountId, JSON.stringify({ id: 'u_a' }))
    AccountStore.setActive(a.accountId)
    expect(AccountStore.activeUserCache()?.id).toBe('u_a')

    AccountStore.setActive(b.accountId)
    expect(AccountStore.activeUserCache()).toBeNull()

    AccountStore.setProfileBlob(b.accountId, JSON.stringify({ id: 'u_b' }))
    expect(AccountStore.activeUserCache()?.id).toBe('u_b')
  })
})

describe('AccountStore — emit / subscribe', () => {
  it('upsert (new), upsert (existing), patch, markSignedOut, setProfileBlob each emit', () => {
    const { AccountStore } = loadStore()
    const fn = jest.fn()
    AccountStore.subscribe(fn)

    // upsert (new)
    fn.mockClear()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    expect(fn).toHaveBeenCalledTimes(1)

    // upsert (existing — same instance + userId)
    fn.mockClear()
    AccountStore.upsert(baseUpsert({ userId: 'u_a', displayName: 'Alpha' }))
    expect(fn).toHaveBeenCalledTimes(1)

    // patch
    fn.mockClear()
    AccountStore.patch(a.accountId, { needsReauth: true })
    expect(fn).toHaveBeenCalledTimes(1)

    // setProfileBlob
    fn.mockClear()
    AccountStore.setProfileBlob(a.accountId, JSON.stringify({ id: 'u_a' }))
    expect(fn).toHaveBeenCalledTimes(1)

    // markSignedOut
    fn.mockClear()
    return AccountStore.markSignedOut(a.accountId).then(() => {
      expect(fn).toHaveBeenCalledTimes(1)
    })
  })

  it('setActive emits at least once and final state is consistent', () => {
    // Note: setActive currently emits twice (once via internal `patch` for
    // `lastActiveAt`, once at the end). We assert ≥1 rather than ===1 so a
    // future R4-style atomic-emit refactor can collapse them without breaking
    // this test — what matters here is that subscribers fire and the final
    // state is consistent.
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setActive(a.accountId)

    const fn = jest.fn()
    AccountStore.subscribe(fn)
    AccountStore.setActive(b.accountId)
    expect(fn.mock.calls.length).toBeGreaterThanOrEqual(1)
    expect(AccountStore.activeId()).toBe(b.accountId)
  })
})

describe('AccountStore — registry capacity & lookup', () => {
  it('upsert throws on the (MAX_ACCOUNTS + 1)th account', () => {
    const { AccountStore, MAX_ACCOUNTS } = loadStore()
    for (let i = 0; i < MAX_ACCOUNTS; i++) {
      AccountStore.upsert(baseUpsert({ userId: `u${i}`, instance: `i${i}.test` }))
    }
    expect(() =>
      AccountStore.upsert(
        baseUpsert({ userId: 'u_overflow', instance: 'overflow.test' })
      )
    ).toThrow(/Cannot add more than/)
  })

  it('upsert of an existing (instance, userId) does not increment count', () => {
    const { AccountStore, MAX_ACCOUNTS } = loadStore()
    for (let i = 0; i < MAX_ACCOUNTS; i++) {
      AccountStore.upsert(baseUpsert({ userId: `u${i}`, instance: `i${i}.test` }))
    }
    expect(AccountStore.list()).toHaveLength(MAX_ACCOUNTS)
    // Re-auth path: hit `upsert` for an already-registered (instance, userId).
    AccountStore.upsert(baseUpsert({ userId: 'u0', instance: 'i0.test' }))
    expect(AccountStore.list()).toHaveLength(MAX_ACCOUNTS)
  })

  it('findByInstanceAndUserId returns the matching account or null', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    expect(AccountStore.findByInstanceAndUserId('a.test', 'u_a')?.accountId).toBe(
      a.accountId
    )
    expect(AccountStore.findByInstanceAndUserId('a.test', 'u_b')).toBeNull()
    expect(AccountStore.findByInstanceAndUserId('c.test', 'u_a')).toBeNull()
  })
})

describe('AccountStore — pickFallbackActiveId', () => {
  it('returns the most-recently-active non-needsReauth account excluding the given id', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    const c = AccountStore.upsert(baseUpsert({ instance: 'c.test', userId: 'u_c' }))
    // Stamp `lastActiveAt` explicitly so the test doesn't depend on the
    // millisecond resolution of `Date.now()` — three rapid setActive() calls
    // can land on the same ms and make the sort unstable.
    AccountStore.patch(a.accountId, { lastActiveAt: 1000 })
    AccountStore.patch(b.accountId, { lastActiveAt: 2000 })
    AccountStore.patch(c.accountId, { lastActiveAt: 3000 })

    // Without exclusion, fallback is c (most recent).
    expect(AccountStore.pickFallbackActiveId()).toBe(c.accountId)
    // Excluding c, fallback drops to b.
    expect(AccountStore.pickFallbackActiveId(c.accountId)).toBe(b.accountId)
    // Mark b as needsReauth — fallback skips to a.
    AccountStore.patch(b.accountId, { needsReauth: true })
    expect(AccountStore.pickFallbackActiveId(c.accountId)).toBe(a.accountId)
  })

  it('returns null when every candidate needsReauth (or none exist)', () => {
    const { AccountStore } = loadStore()
    expect(AccountStore.pickFallbackActiveId()).toBeNull()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.patch(a.accountId, { needsReauth: true })
    expect(AccountStore.pickFallbackActiveId()).toBeNull()
  })

  it('skips dormant (signedOut) accounts even when they have a fresh lastActiveAt', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.patch(a.accountId, { lastActiveAt: 1000 })
    AccountStore.patch(b.accountId, { lastActiveAt: 9000 })
    // Dormant the most-recent candidate; fallback must drop to the
    // remaining live entry rather than picking the soft-deleted one.
    await AccountStore.markSignedOut(b.accountId)
    expect(AccountStore.pickFallbackActiveId()).toBe(a.accountId)
  })
})

describe('AccountStore — markSignedOut (soft-delete)', () => {
  it('hides the account from list(), clears activeId, but preserves the user MMKV slot for warm re-auth', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.setActive(a.accountId)
    AccountStore.setProfileBlob(a.accountId, JSON.stringify({ id: 'u_a' }))
    AccountStore.markCredentialsVerified(a.accountId)

    await AccountStore.markSignedOut(a.accountId)

    // Hidden from the visible list…
    expect(AccountStore.list()).toHaveLength(0)
    expect(AccountStore.activeId()).toBeNull()
    // …but still findable via instance+userId so a re-auth revives the
    // same accountId.
    expect(AccountStore.findByInstanceAndUserId('example.test', 'u_a')?.accountId).toBe(
      a.accountId
    )
    // Profile blob and verification timestamp survive — the whole point
    // of soft-delete is that re-signing in lands warm.
    const userMmkv = mockMmkv.get('user')
    expect(userMmkv?.getString(`user.account.${a.accountId}.profile`)).toBeDefined()
    expect(
      userMmkv?.getNumber(`user.account.${a.accountId}.credentials_verified_at`)
    ).toBeDefined()
  })

  it('drops per-account tokens and preserves per-instance OAuth credentials in every case', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'solo.test', userId: 'u_a' }))
    mockSecure.setItem(`secure.account.${a.accountId}.access_token`, 'tok-a')
    mockSecure.setItem(`secure.account.${a.accountId}.refresh_token`, 'ref-a')
    mockSecure.setItem(`secure.instance.solo.test.client_id`, 'client-solo')
    mockSecure.setItem(`secure.instance.solo.test.client_secret`, 'secret-solo')

    await AccountStore.markSignedOut(a.accountId)

    // Tokens go.
    expect(mockSecure.has(`secure.account.${a.accountId}.access_token`)).toBe(false)
    expect(mockSecure.has(`secure.account.${a.accountId}.refresh_token`)).toBe(false)
    // Per-instance OAuth credentials stay — soft-deleted accounts still
    // count as on that instance, and keeping the credentials lets a
    // re-sign-in skip the /api/v1/apps registration round-trip.
    expect(mockSecure.has(`secure.instance.solo.test.client_id`)).toBe(true)
    expect(mockSecure.has(`secure.instance.solo.test.client_secret`)).toBe(true)
  })

  it('preserves activeId when soft-deleting a non-active account', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setActive(a.accountId)
    await AccountStore.markSignedOut(b.accountId)
    expect(AccountStore.activeId()).toBe(a.accountId)
    expect(AccountStore.list().map((x) => x.accountId)).toEqual([a.accountId])
  })

  it('flips the signedOut flag and patches needsReauth/tokenExpiresAt/pushRegisteredAt to null', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.patch(a.accountId, {
      needsReauth: true,
      tokenExpiresAt: 12345,
      pushRegisteredAt: 67890,
    })
    await AccountStore.markSignedOut(a.accountId)

    const dormant = AccountStore.findByInstanceAndUserId('example.test', 'u_a')
    expect(dormant?.signedOut).toBe(true)
    expect(dormant?.needsReauth).toBe(false)
    expect(dormant?.tokenExpiresAt).toBeNull()
    expect(dormant?.pushRegisteredAt).toBeNull()
  })

  it('upsert revives a dormant entry — same accountId, signedOut cleared', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.setProfileBlob(a.accountId, JSON.stringify({ id: 'u_a' }))
    await AccountStore.markSignedOut(a.accountId)

    const revived = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    expect(revived.accountId).toBe(a.accountId)
    expect(revived.signedOut).toBe(false)
    // Visible-list contains exactly the revived entry.
    expect(AccountStore.list().map((x) => x.accountId)).toEqual([a.accountId])
    // Cached profile blob still readable — the warm re-auth invariant.
    const userMmkv = mockMmkv.get('user')
    expect(userMmkv?.getString(`user.account.${a.accountId}.profile`)).toBeDefined()
  })

  it('is idempotent — a second markSignedOut on a dormant entry is a no-op', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    await AccountStore.markSignedOut(a.accountId)

    const fn = jest.fn()
    AccountStore.subscribe(fn)
    await AccountStore.markSignedOut(a.accountId)
    expect(fn).not.toHaveBeenCalled()
  })

  it('clearAll wipes dormant entries along with everything else', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    AccountStore.setProfileBlob(a.accountId, JSON.stringify({ id: 'u_a' }))
    await AccountStore.markSignedOut(a.accountId)
    expect(AccountStore.findByInstanceAndUserId('a.test', 'u_a')).not.toBeNull()

    await AccountStore.clearAll()

    expect(AccountStore.findByInstanceAndUserId('a.test', 'u_a')).toBeNull()
    const userMmkv = mockMmkv.get('user')
    expect(userMmkv?.getString(`user.account.${a.accountId}.profile`)).toBeUndefined()
  })
})

describe('AccountStore — clearActive', () => {
  it('clears the active pointer without mutating the registry', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.setActive(a.accountId)

    AccountStore.clearActive()

    expect(AccountStore.activeId()).toBeNull()
    expect(AccountStore.activeAccount()).toBeNull()
    // Entry survives — auth-failure no-fallback path lands the user on
    // /(public)/login but keeps the registry entry around for a warm
    // re-auth via upsert()'s (instance, userId) match.
    expect(AccountStore.findByInstanceAndUserId('example.test', 'u_a')?.accountId).toBe(
      a.accountId
    )
  })

  it('is a no-op when no account is active', () => {
    const { AccountStore } = loadStore()
    const fn = jest.fn()
    AccountStore.subscribe(fn)
    AccountStore.clearActive()
    expect(fn).not.toHaveBeenCalled()
  })
})

describe('AccountStore — markSignedOut atomic swap (R4)', () => {
  it('emits exactly once with (newList, fallbackId) — no (newList, null) intermediate', async () => {
    // Pre-R4 shape was: clear activeId, emit, then `setActive(fallback)`,
    // emit again — subscribers briefly observed `activeId === null` while
    // the visible list had already shrunk, and any key built from `acct()`
    // in that window threw. R4 collapses that to a single emit by
    // accepting the fallback inline; soft-delete preserves the invariant.
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setActive(a.accountId)

    const observed: Array<{ list: number; activeId: string | null }> = []
    const fn = jest.fn(() => {
      observed.push({
        list: AccountStore.list().length,
        activeId: AccountStore.activeId(),
      })
    })
    AccountStore.subscribe(fn)

    await AccountStore.markSignedOut(a.accountId, b.accountId)

    expect(fn).toHaveBeenCalledTimes(1)
    expect(observed).toEqual([{ list: 1, activeId: b.accountId }])
    expect(AccountStore.activeId()).toBe(b.accountId)
    expect(AccountStore.list().map((x) => x.accountId)).toEqual([b.accountId])
  })

  it('without a fallback, signing out the active account clears activeId', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.setActive(a.accountId)
    await AccountStore.markSignedOut(a.accountId, null)
    expect(AccountStore.activeId()).toBeNull()
    expect(AccountStore.activeAccount()).toBeNull()
    expect(AccountStore.activeUserCache()).toBeNull()
  })

  it('a stale fallback (not in the post-mutation list) is ignored, activeId clears', async () => {
    // Defensive: an external caller passing an accountId that isn't in the
    // registry must not poison the active pointer. Falling through to "clear"
    // is the same shape as passing null/undefined.
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.setActive(a.accountId)
    await AccountStore.markSignedOut(a.accountId, 'never-existed')
    expect(AccountStore.activeId()).toBeNull()
  })

  it('a fallback that points at the about-to-be-dormant account is ignored', async () => {
    // Pathological self-fallback: if the caller passes the same id being
    // soft-deleted, the swap path must not write back a now-dormant entry.
    // The `next.some(... && !a.signedOut)` filter catches this.
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ userId: 'u_a' }))
    AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setActive(a.accountId)
    await AccountStore.markSignedOut(a.accountId, a.accountId)
    expect(AccountStore.activeId()).toBeNull()
  })

  it('fallback is ignored when soft-deleting a non-active account', async () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    const c = AccountStore.upsert(baseUpsert({ instance: 'c.test', userId: 'u_c' }))
    AccountStore.setActive(a.accountId)
    // Soft-deleting b while a is active — the fallback parameter only
    // takes effect when the soft-deleted id IS the active id, so passing
    // c here is a no-op and a stays active.
    await AccountStore.markSignedOut(b.accountId, c.accountId)
    expect(AccountStore.activeId()).toBe(a.accountId)
    expect(AccountStore.list().map((x) => x.accountId)).toEqual([a.accountId, c.accountId])
  })
})

describe('AccountStore — setProfileBlob target', () => {
  it('writes to the given accountId regardless of which account is active', () => {
    const { AccountStore } = loadStore()
    const a = AccountStore.upsert(baseUpsert({ instance: 'a.test', userId: 'u_a' }))
    const b = AccountStore.upsert(baseUpsert({ instance: 'b.test', userId: 'u_b' }))
    AccountStore.setActive(a.accountId)
    AccountStore.setProfileBlob(b.accountId, JSON.stringify({ id: 'u_b', username: 'bob' }))

    const userMmkv = mockMmkv.get('user')
    // B's slot is populated; A's stays untouched.
    expect(userMmkv?.getString(`user.account.${b.accountId}.profile`)).toBeDefined()
    expect(userMmkv?.getString(`user.account.${a.accountId}.profile`)).toBeUndefined()

    // Switch to B and verify the blob is observable as the new userCache.
    AccountStore.setActive(b.accountId)
    expect(AccountStore.activeUserCache()?.id).toBe('u_b')
  })
})
