import { describe, it, expect } from 'vitest'
import { isHealthEvent, healthFromEvent, applyHealthEvent, applyHealthEventTo } from './health'
import type { Entity } from './api'
import type { OikosEvent } from './stores/events'

function ev(partial: Partial<OikosEvent>): OikosEvent {
  return {
    id: 1,
    ts: '2026-07-28T12:00:00Z',
    type: 'health.changed',
    entity_id: 'e1',
    severity: 'info',
    source: 'scheduler',
    data: { to: 'degraded' },
    correlation_id: null,
    ...partial
  } as OikosEvent
}

function entity(partial: Partial<Entity> = {}): Entity {
  return {
    id: 'e1',
    slug: 'lxc:apps',
    type: 'lxc',
    name: 'apps',
    attributes: {},
    version: 1,
    created_at: '',
    updated_at: '',
    health: 'healthy',
    ...partial
  } as Entity
}

describe('health event application', () => {
  it('reads the new health out of the payload', () => {
    expect(healthFromEvent(ev({}))).toBe('degraded')
    expect(healthFromEvent(ev({ type: 'health.stale', data: { to: 'stale' } }))).toBe('stale')
  })

  it('ignores events that are not health events', () => {
    expect(isHealthEvent(ev({ type: 'signal.raised' }))).toBe(false)
    expect(healthFromEvent(ev({ type: 'execution.output' }))).toBeNull()
  })

  it('ignores a health event with no usable payload', () => {
    expect(healthFromEvent(ev({ data: {} }))).toBeNull()
    expect(healthFromEvent(ev({ entity_id: null }))).toBeNull()
  })

  it('patches the matching entity in a list and leaves the rest alone', () => {
    const list = [entity(), entity({ id: 'e2', slug: 'lxc:dns', health: 'healthy' })]
    const next = applyHealthEvent(list, ev({}))
    expect(next[0].health).toBe('degraded')
    expect(next[0].last_check_at).toBe('2026-07-28T12:00:00Z')
    // untouched entities keep their identity, so rows that did not change do
    // not re-render
    expect(next[1]).toBe(list[1])
  })

  // Returning the same array reference matters: assigning a fresh array on
  // every unrelated event would churn the whole table.
  it('returns the original array when the event does not apply', () => {
    const list = [entity()]
    expect(applyHealthEvent(list, ev({ entity_id: 'nobody' }))).toBe(list)
    expect(applyHealthEvent(list, ev({ type: 'signal.raised' }))).toBe(list)
    expect(applyHealthEvent(list, ev({ data: { to: 'healthy' } }))).toBe(list) // already healthy
  })

  it('patches a single entity for a detail view', () => {
    const e = entity()
    const next = applyHealthEventTo(e, ev({}))
    expect(next?.health).toBe('degraded')
    expect(applyHealthEventTo(e, ev({ entity_id: 'other' }))).toBe(e)
    expect(applyHealthEventTo(null, ev({}))).toBeNull()
  })

  // The scheduler emits health.changed with entity_id = the observed entity
  // but data.slug = the *check's* slug, so slug must never be used to match.
  it('matches on entity_id, never on the payload slug', () => {
    const list = [entity()]
    const next = applyHealthEvent(list, ev({ data: { to: 'down', slug: 'check:ping:whatever:0' } }))
    expect(next[0].health).toBe('down')
  })
})
