/**
 * SessionsService: list store projection (manager → {ids, byId} with derived
 * titles), scope-tree lifecycle (lazy mint / frozen survival / removed
 * teardown with watch deferral), binding identity, ancestry walk, create.
 */
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
import { SessionsService, scopeOf } from '../src/client/sessions/service.ts'
import { FakeApiClient, ok } from './fake-api.ts'

const sid = (s: string): SessionId => s as SessionId

interface Bench {
  ctx: Context
  api: FakeApiClient
  svc: SessionsService
}

function bench(): Bench {
  const ctx = new Context()
  const api = new FakeApiClient()
  const svc = new SessionsService(ctx, api)
  return { ctx, api, svc }
}

/** Refresh the manager list from programmable rows and flush the microtask batch. */
async function feedList(b: Bench, rows: { id: string; cwd?: string; parentId?: string; running?: boolean }[]): Promise<void> {
  b.api.onList = () => Promise.resolve(ok({
    items: rows.map(r => ({
      sessionId: sid(r.id), updatedAt: 1, running: r.running ?? false,
      ...(r.cwd !== undefined ? { cwd: r.cwd } : {}),
      ...(r.parentId !== undefined ? { parentSessionId: sid(r.parentId) } : {}),
    })),
  }) as never)
  await b.svc.manager.refreshList()
  await Promise.resolve() // manager notifier flush
}

describe('list store projection', () => {
  it('projects ids/byId with cwd-basename titles (id fallback) and parent links', async () => {
    const b = bench()
    await feedList(b, [
      { id: 's1', cwd: '/home/u/proj-a/' },
      { id: 's2', parentId: 's1', running: true },
    ])
    const state = b.svc.list.getSnapshot()
    expect(state.ids).toEqual(['s1', 's2'])
    expect(state.byId[sid('s1')]).toMatchObject({ title: 'proj-a', cwd: '/home/u/proj-a/' })
    expect(state.byId[sid('s2')]).toMatchObject({ title: 's2', parentId: 's1', running: true })
  })

  it('reflects live increments (host stream via manager) into the store', async () => {
    const b = bench()
    await feedList(b, [{ id: 's1' }])
    b.svc.manager.handleHostEnvelope({ rpcId: 'r1' as never, payload: { type: 'host/session-added', sessionId: sid('s2') } as never })
    await Promise.resolve()
    expect(b.svc.list.getSnapshot().ids).toContain('s2')
  })
})

describe('scope tree', () => {
  it('mints lazily on first resolution, tags the ctx, and keeps binding identity stable', async () => {
    const b = bench()
    await feedList(b, [{ id: 's1' }])
    expect(b.svc.scope(sid('unknown'))).toBeUndefined()
    const scoped = b.svc.scope(sid('s1'))
    expect(scoped).toBeDefined()
    expect(scopeOf(scoped as Context)).toBe('s1')
    expect(scopeOf(b.ctx)).toBeUndefined()
    const binding = b.svc.binding(sid('s1'))
    expect(binding?.session).toBe(b.svc.manager.get(sid('s1')))
    expect(b.svc.binding(sid('s1'))).toBe(binding)
    expect(binding?.ctx).toBe(scoped)
  })

  it('tears down an unwatched removed session but defers the watched one until the watch moves', async () => {
    const b = bench()
    await feedList(b, [{ id: 's1' }, { id: 's2' }])
    const ctx1 = b.svc.scope(sid('s1'))
    b.svc.binding(sid('s1')) // s1 is watched
    b.svc.scope(sid('s2')) // s2 scoped but not watched

    await feedList(b, [{ id: 's1' }]) // s2 removed, unwatched: torn down
    expect(b.svc.scope(sid('s2'))).toBeUndefined()

    await feedList(b, []) // s1 removed while watched: deferred, scope survives
    expect(b.svc.scope(sid('s1'))).toBe(ctx1)

    await feedList(b, [{ id: 's3' }])
    b.svc.binding(sid('s3')) // watch moves: deferred teardown sweeps s1
    expect(b.svc.scope(sid('s1'))).toBeUndefined()
  })

  it('keeps the scope when the session merely stops running (frozen ≠ removed)', async () => {
    const b = bench()
    await feedList(b, [{ id: 's1', running: true }])
    const scoped = b.svc.scope(sid('s1'))
    await feedList(b, [{ id: 's1', running: false }])
    expect(b.svc.scope(sid('s1'))).toBe(scoped)
  })

  it('cancels a deferred teardown when the id reappears in the list', async () => {
    const b = bench()
    await feedList(b, [{ id: 's1' }])
    const scoped = b.svc.scope(sid('s1'))
    b.svc.binding(sid('s1'))
    await feedList(b, []) // removed while watched → deferred
    await feedList(b, [{ id: 's1' }, { id: 's2' }]) // reappears
    b.svc.binding(sid('s2')) // watch moves; sweep must NOT tear down the re-listed s1
    expect(b.svc.scope(sid('s1'))).toBe(scoped)
  })
})

describe('ancestry', () => {
  it('walks parentId links root-first including self; broken links stop the walk', async () => {
    const b = bench()
    await feedList(b, [
      { id: 'root', cwd: '/w/app' },
      { id: 'mid', parentId: 'root' },
      { id: 'leaf', parentId: 'mid' },
      { id: 'orphan', parentId: 'ghost' },
    ])
    expect(b.svc.ancestry(sid('leaf')).map(s => s.id)).toEqual(['root', 'mid', 'leaf'])
    expect(b.svc.ancestry(sid('orphan')).map(s => s.id)).toEqual(['orphan'])
    expect(b.svc.ancestry(sid('ghost'))).toEqual([])
  })
})

describe('create', () => {
  it('returns the new id on ok and throws a coded error on failure', async () => {
    const b = bench()
    b.api.onCreate = () => Promise.resolve(ok({ sessionId: sid('fresh') }))
    await expect(b.svc.create({ cwd: '/w' })).resolves.toBe('fresh')
    b.api.onCreate = () => Promise.resolve({
      rpcId: 'e' as never,
      result: { ok: false as const, error: { code: 'internal' as const, message: '爆了', details: {} } },
    } as never)
    await expect(b.svc.create()).rejects.toThrow(/internal: 爆了/)
  })
})
