import { describe, expect, it, vi } from 'vitest'
import type { RpcResponse } from '@deepseek-ai/dsh-client-connection/client'
import { WelcomeNoticeStore } from '../src/client/welcome-store.ts'
import { refreshWelcomeIfLoaded } from '../src/client/welcome-store.ts'
import {
  WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION,
} from '../src/onboarding-copy.ts'

let rpc = 0
function ok<T>(value: T): RpcResponse<T> {
  return { rpcId: `welcome-${rpc++}` as never, result: { ok: true, value } }
}

function namespace(version?: string) {
  return {
    ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
    schema: {},
    value: version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version },
    applies: 'live' as const,
    secrets: [],
    revision: 0,
  }
}

function deferred<T>() {
  let resolve!: (value: T) => void
  let reject!: (reason: unknown) => void
  const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
  return { promise, resolve, reject }
}

describe('WelcomeNoticeStore', () => {
  it('acknowledges in memory without calling loopback-only settings APIs', async () => {
    const describe = vi.fn()
    const mutate = vi.fn()
    const controller = new WelcomeNoticeStore({ settings: { describe, mutate } } as never, 'memory')

    await controller.load()
    expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: false, error: null })
    await expect(controller.acknowledge()).resolves.toBe(true)
    expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: true, error: null })
    await controller.load()
    expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: true, error: null })
    expect(describe).not.toHaveBeenCalled()
    expect(mutate).not.toHaveBeenCalled()
  })

  it('acknowledges only the exact current copy version', async () => {
    for (const [version, acknowledged] of [
      [undefined, false],
      ['older-copy', false],
      [WELCOME_NOTICE_VERSION, true],
    ] as const) {
      const api = {
        settings: {
          describe: vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace(version)] }))),
        },
      }
      const controller = new WelcomeNoticeStore(api as never)
      await controller.load()
      expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged })
    }
  })

  it('persists the owner version through one idempotent path mutation', async () => {
    const mutate = vi.fn(() => Promise.resolve(ok(namespace(WELCOME_NOTICE_VERSION))))
    const controller = new WelcomeNoticeStore({ settings: { mutate } } as never)
    await expect(controller.acknowledge()).resolves.toBe(true)
    expect(mutate).toHaveBeenCalledWith({
      ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
      ops: [{ op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION }],
    })
    expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true })
  })

  it('keeps the notice pending when loading or persistence fails', async () => {
    const load = new WelcomeNoticeStore({
      settings: { describe: () => Promise.reject(new Error('offline')) },
    } as never)
    await load.load()
    expect(load.store.getSnapshot()).toEqual({ status: 'error', acknowledged: false, error: 'offline' })

    const save = new WelcomeNoticeStore({
      settings: { mutate: () => Promise.reject(new Error('disk full')) },
    } as never)
    await expect(save.acknowledge()).resolves.toBe(false)
    expect(save.store.getSnapshot()).toEqual({ status: 'error', acknowledged: false, error: 'disk full' })

    const nonError = new WelcomeNoticeStore({
      // Durable/wire failures are unknown; exercise containment of a non-Error rejection.
      // oxlint-disable-next-line typescript/prefer-promise-reject-errors
      settings: { describe: () => Promise.reject('offline string') },
    } as never)
    await nonError.load()
    expect(nonError.store.getSnapshot().error).toBe('offline string')
  })

  it('reports business failures, missing namespaces, and malformed durable values', async () => {
    for (const describe of [
      () => Promise.resolve({
        rpcId: 'failed' as never,
        result: { ok: false as const, error: { code: 'internal' as const, message: 'denied', details: {} } },
      }),
      () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] })),
    ]) {
      const controller = new WelcomeNoticeStore({ settings: { describe } } as never)
      await controller.load()
      expect(controller.store.getSnapshot().status).toBe('error')
    }

    for (const value of [null, 42, { [WELCOME_NOTICE_ACK_FIELD]: 42 }]) {
      const controller = new WelcomeNoticeStore({
        settings: { describe: () => Promise.resolve(ok({
          writable: true,
          hasDocument: false,
          namespaces: [{ ...namespace(), value }],
        })) },
      } as never)
      await controller.load()
      expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: false })
    }

    const save = new WelcomeNoticeStore({
      settings: { mutate: () => Promise.resolve({
        rpcId: 'failed-save' as never,
        result: { ok: false, error: { code: 'settings-rejected', message: 'denied', details: { ns: WELCOME_NOTICE_SETTINGS_NAMESPACE } } },
      }) },
    } as never)
    await expect(save.acknowledge()).resolves.toBe(false)
    expect(save.store.getSnapshot().error).toBe('denied')
  })

  it('lets the latest load win over stale success and failure', async () => {
    const first = deferred<ReturnType<typeof ok>>()
    const describe = vi.fn()
      .mockImplementationOnce(() => first.promise)
      .mockImplementationOnce(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace()] })))
    const controller = new WelcomeNoticeStore({ settings: { describe } } as never)
    const stale = controller.load()
    await controller.load()
    first.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace(WELCOME_NOTICE_VERSION)] }))
    await stale
    expect(controller.store.getSnapshot().acknowledged).toBe(false)

    const failed = deferred<ReturnType<typeof ok>>()
    describe
      .mockImplementationOnce(() => failed.promise)
      .mockImplementationOnce(() => Promise.resolve(ok({
        writable: true, hasDocument: false, namespaces: [namespace(WELCOME_NOTICE_VERSION)],
      })))
    const staleFailure = controller.load()
    await controller.load()
    failed.reject('stale failure')
    await staleFailure
    expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true, error: null })
  })

  it('contains stale acknowledgement settlements and refreshes only a loaded store', async () => {
    const write = deferred<ReturnType<typeof ok>>()
    const describe = vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace()] })))
    const controller = new WelcomeNoticeStore({
      settings: { mutate: () => write.promise, describe },
    } as never)
    refreshWelcomeIfLoaded(controller)
    expect(describe).not.toHaveBeenCalled()
    const staleWrite = controller.acknowledge()
    await controller.load()
    write.resolve(ok(namespace(WELCOME_NOTICE_VERSION)))
    await expect(staleWrite).resolves.toBe(true)
    expect(controller.store.getSnapshot().acknowledged).toBe(false)
    refreshWelcomeIfLoaded(controller)
    await vi.waitFor(() => { expect(describe).toHaveBeenCalledTimes(2) })

    const failedWrite = deferred<ReturnType<typeof ok>>()
    const staleFailure = new WelcomeNoticeStore({
      settings: { mutate: () => failedWrite.promise, describe },
    } as never)
    const pending = staleFailure.acknowledge()
    await staleFailure.load()
    failedWrite.reject('late failure')
    await expect(pending).resolves.toBe(false)
    expect(staleFailure.store.getSnapshot().status).toBe('ready')
  })
})
