// @vitest-environment jsdom
/**
 * ui-openrouter-usage browser half on a real cordis Context with fake slots/
 * locale/remote faces: the plugin registers the CostDock entry at
 * conversation.composer.dock and the BalanceBadge entry at
 * sidebar.footer.action, the balance snapshot reads roll through the injected
 * face at call time, and registration disposal rides the plugin fiber (HMR
 * safety). The node half and the invariant companion are exercised over the
 * same Context.
 */
import { Context, Service } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import type { OpenRouterBalance } from '@deepseek-ai/dsh-openrouter-usage/client'
import type { BalanceBadgeActions } from '../src/client/BalanceBadge.tsx'
import { BalanceBadge } from '../src/client/BalanceBadge.tsx'
import { CostDock } from '../src/client/CostDock.tsx'
import { apply, inject, NS } from '../src/client/index.ts'
import { apply as nodeApply } from '../src/index.ts'

afterEach(cleanup)

async function bench() {
  const ctx = new Context()
  await ctx.plugin(SlotRegistry).await()
  const locale = new LocaleRuntime(ctx)
  ctx.provide('locale', locale)
  class RemoteService extends Service {
    constructor(serviceCtx: Context) {
      super(serviceCtx, 'remote')
    }
  }
  new RemoteService(ctx)
  const snapshot = vi.fn<() => Promise<RemoteResult<OpenRouterBalance>>>()
    .mockResolvedValue({ ok: true, value: balance() })
  const refresh = vi.fn<() => Promise<RemoteResult<OpenRouterBalance>>>()
    .mockResolvedValue({ ok: true, value: balance({ balanceUsd: 9 }) })
  ctx.provide('remote.openRouterUsage', { snapshot, refresh })
  return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, snapshot, refresh }
}

function declare(slots: SlotRegistry): () => void {
  return slots.register({
    name: 'root',
    children: {
      'conversation.composer.dock': { kind: 'list', scope: 'session' },
      'sidebar.footer.action': { kind: 'list', scope: 'root' },
    },
  } as never, () => null)
}

function balance(over: Partial<OpenRouterBalance> = {}): OpenRouterBalance {
  return {
    balanceUsd: 5.5,
    label: 'k',
    usageTokens: 0,
    limitTokens: 0,
    isFreeTier: false,
    updatedAt: 1_700_000_000_000,
    currency: 'USD',
    ...over,
  }
}

describe('ui-openrouter-usage browser plugin', () => {
  it('declares only the services its two entries read', () => {
    expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.openRouterUsage'])
  })

  it('registers localized cost and balance entries without reading the Remote eagerly', async () => {
    const b = await bench()
    declare(b.slots)
    await b.ctx.plugin({ inject: [...inject], apply }).await()

    const cost = b.slots.entries('conversation.composer.dock')[0]!
    expect(cost.component).toBe(CostDock)
    expect(cost.options).toMatchObject({ id: 'openrouter-cost', order: 5 })
    expect(cost.locale).toBe(NS)

    const badge = b.slots.entries('sidebar.footer.action')[0]!
    expect(badge.component).toBe(BalanceBadge)
    expect(badge.options).toMatchObject({ id: 'openrouter-balance', order: 10 })
    expect(badge.locale).toBe(NS)

    // Registration reads nothing live; both reads roll at call time.
    expect(b.snapshot).not.toHaveBeenCalled()
    expect(b.refresh).not.toHaveBeenCalled()
    const injected = (badge.inject as unknown as () => BalanceBadgeActions)()
    await expect(injected.snapshot()).resolves.toEqual(balance())
    expect(b.snapshot).toHaveBeenCalledOnce()
    await expect(injected.refresh()).resolves.toEqual(balance({ balanceUsd: 9 }))
    expect(b.refresh).toHaveBeenCalledOnce()
    await b.ctx.fiber.dispose()
  })

  it('forwards a Remote failure out of the injected snapshot verbatim', async () => {
    const b = await bench()
    declare(b.slots)
    await b.ctx.plugin({ inject: [...inject], apply }).await()
    b.snapshot.mockResolvedValueOnce({ ok: false, error: { code: 'REMOTE_ERROR', message: 'boom', details: {} } })
    const injected = (b.slots.entries('sidebar.footer.action')[0]!.inject as unknown as () => BalanceBadgeActions)()
    await expect(injected.snapshot()).rejects.toThrow('openRouterUsage.snapshot failed: REMOTE_ERROR: boom')
    await b.ctx.fiber.dispose()
  })

  it('forwards a Remote failure out of the injected refresh verbatim', async () => {
    const b = await bench()
    declare(b.slots)
    await b.ctx.plugin({ inject: [...inject], apply }).await()
    b.refresh.mockResolvedValueOnce({ ok: false, error: { code: 'REMOTE_ERROR', message: 'nope', details: {} } })
    const injected = (b.slots.entries('sidebar.footer.action')[0]!.inject as unknown as () => BalanceBadgeActions)()
    await expect(injected.refresh()).rejects.toThrow('openRouterUsage.refresh failed: REMOTE_ERROR: nope')
    await b.ctx.fiber.dispose()
  })

  it('follows locale and recovers across late declaration and declarer reload', async () => {
    const b = await bench()
    const fiber = b.ctx.plugin({ inject: [...inject], apply })
    await fiber.await()
    expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0)
    expect(b.slots.entries('sidebar.footer.action')).toHaveLength(0)

    const stop = declare(b.slots)
    await vi.waitFor(() => { expect(b.slots.entries('conversation.composer.dock')).toHaveLength(1) })
    expect(b.slots.entries('sidebar.footer.action')).toHaveLength(1)

    stop()
    expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0)
    declare(b.slots)
    await vi.waitFor(() => {
      expect(b.slots.entries('sidebar.footer.action')[0]?.component).toBe(BalanceBadge)
    })

    await fiber.dispose()
    expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0)
    expect(b.slots.entries('sidebar.footer.action')).toHaveLength(0)
    await b.ctx.fiber.dispose()
  })
})

describe('ui-openrouter-usage node half', () => {
  it('the node apply is an inert loader seat', () => {
    expect(() => { nodeApply() }).not.toThrow()
  })
})
