import { describe, it, expect, beforeEach, vi } from 'vitest'
import { get, writable } from 'svelte/store'
import type { OikosEvent } from './events'

// The store is driven entirely by SSE events plus a log fetch, so both are
// mocked. What matters is the correlation logic: an execution.output event is
// matched to a chat session by correlation_id, which MCP-initiated executions
// now carry (it used to be a random per-execution UUID that correlated
// nothing).

const liveEvents = writable<OikosEvent[]>([])
const subscribeEvents = vi.fn(() => () => {})
const fetchExecutionLogs = vi.fn(async (id: string) => ({
  items: [],
  combined: `output-for-${id}`
}))

vi.mock('./events', () => ({
  liveEvents,
  subscribeEvents
}))
vi.mock('$lib/api', () => ({
  fetchExecutionLogs: (id: string) => fetchExecutionLogs(id)
}))

let mod: typeof import('./execstream')

function event(partial: Partial<OikosEvent>): OikosEvent {
  return {
    id: Math.floor(Math.random() * 1e9),
    ts: new Date().toISOString(),
    type: 'execution.output',
    entity_id: 'exec-1',
    severity: 'info',
    source: 'actuator',
    data: {},
    correlation_id: 'session-1',
    ...partial
  } as OikosEvent
}

// The store fetches asynchronously; let the microtask queue drain.
const settle = () => new Promise((r) => setTimeout(r, 0))

beforeEach(async () => {
  liveEvents.set([])
  fetchExecutionLogs.mockClear()
  vi.resetModules()
  mod = await import('./execstream')
})

describe('liveExecutionOutputFor', () => {
  it('picks up output for its own session', async () => {
    const store = mod.liveExecutionOutputFor('session-1')
    const stop = store.subscribe(() => {})

    liveEvents.set([event({ entity_id: 'exec-1', correlation_id: 'session-1' })])
    await settle()

    expect(fetchExecutionLogs).toHaveBeenCalledWith('exec-1')
    expect(get(store)).toEqual({ executionId: 'exec-1', output: 'output-for-exec-1' })
    stop()
  })

  // Without this every open chat window would tail every other session's
  // commands.
  it('ignores output belonging to a different session', async () => {
    const store = mod.liveExecutionOutputFor('session-1')
    const stop = store.subscribe(() => {})

    liveEvents.set([event({ entity_id: 'exec-9', correlation_id: 'session-2' })])
    await settle()

    expect(fetchExecutionLogs).not.toHaveBeenCalled()
    expect(get(store)).toBeNull()
    stop()
  })

  it('ignores unrelated event types', async () => {
    const store = mod.liveExecutionOutputFor('session-1')
    const stop = store.subscribe(() => {})

    liveEvents.set([event({ type: 'signal.raised' })])
    await settle()

    expect(fetchExecutionLogs).not.toHaveBeenCalled()
    stop()
  })

  // A session runs commands one after another; the second must not inherit
  // the first one's output.
  it('resets when a new execution starts in the same session', async () => {
    const store = mod.liveExecutionOutputFor('session-1')
    const stop = store.subscribe(() => {})

    liveEvents.set([event({ entity_id: 'exec-1' })])
    await settle()
    expect(get(store)?.executionId).toBe('exec-1')

    liveEvents.set([event({ entity_id: 'exec-2' })])
    await settle()
    expect(get(store)).toEqual({ executionId: 'exec-2', output: 'output-for-exec-2' })
    stop()
  })

  // Once the command finishes its output belongs to the tool_result, not to a
  // still-"running" entry — leaving it set would show stale output against
  // the next command.
  it('clears on a terminal execution event', async () => {
    const store = mod.liveExecutionOutputFor('session-1')
    const stop = store.subscribe(() => {})

    liveEvents.set([event({ entity_id: 'exec-1' })])
    await settle()
    expect(get(store)).not.toBeNull()

    liveEvents.set([event({ type: 'execution.completed', entity_id: 'exec-1' })])
    await settle()
    expect(get(store)).toBeNull()
    stop()
  })

  it('does not clear on another session completing', async () => {
    const store = mod.liveExecutionOutputFor('session-1')
    const stop = store.subscribe(() => {})

    liveEvents.set([event({ entity_id: 'exec-1' })])
    await settle()

    liveEvents.set([
      event({ type: 'execution.completed', entity_id: 'exec-5', correlation_id: 'session-2' })
    ])
    await settle()
    expect(get(store)).not.toBeNull()
    stop()
  })
})
