import { describe, expect, it } from 'vitest'
import { bySystem, byOntology, byKind, byOwner, governs, governedBy, parsePath, omSearch, omDescribe } from '../queries'
import { byOntology as publicByOntology } from '../index'
import { formatText, formatJson, formatIdsOnly, formatOmSearchHits, formatOmDescribe } from '../format'
import type { OrganizationGraph } from '../../organization-model/graph/types'
import type { OrgKnowledgeNode } from '../../organization-model/domains/knowledge'
import type { OrganizationModel } from '../../organization-model/types'

// ---------------------------------------------------------------------------
// Synthetic fixtures
// ---------------------------------------------------------------------------

/**
 * Three knowledge nodes covering all three kinds.
 * - playbook-a governs system:sales.crm and system:sales.lead-gen
 * - strategy-b governs system:sales.lead-gen
 * - reference-c has no links (no governs edges)
 * - playbook-d is owned by "role.ops-lead"
 */
const NODES: OrgKnowledgeNode[] = [
  {
    id: 'knowledge.playbook-a',
    kind: 'playbook',
    title: 'Playbook A',
    summary: 'Playbook A summary.',
    body: '## Playbook A\n\nContent.',
    links: [{ nodeId: 'system:sales.crm' }, { nodeId: 'system:sales.lead-gen' }],
    ownerIds: [],
    updatedAt: '2026-01-01'
  },
  {
    id: 'knowledge.strategy-b',
    kind: 'strategy',
    title: 'Strategy B',
    summary: 'Strategy B summary.',
    body: '## Strategy B\n\nContent.',
    links: [{ nodeId: 'system:sales.lead-gen' }],
    ownerIds: ['role.ops-lead'],
    updatedAt: '2026-01-02'
  },
  {
    id: 'knowledge.reference-c',
    kind: 'reference',
    title: 'Reference C',
    summary: 'Reference C summary.',
    body: '## Reference C\n\nContent.',
    links: [],
    ownerIds: [],
    updatedAt: '2026-01-03'
  },
  {
    id: 'knowledge.playbook-d',
    kind: 'playbook',
    title: 'Playbook D',
    summary: 'Playbook D summary.',
    body: '## Playbook D\n\nContent.',
    links: [],
    ownerIds: ['role.ops-lead', 'role.ceo'],
    updatedAt: '2026-01-04'
  }
]

/**
 * Minimal synthetic OrganizationGraph derived from the above nodes.
 * Graph node IDs follow the convention: `knowledge:<om-node-id>`
 */
const GRAPH: OrganizationGraph = {
  version: 1,
  organizationModelVersion: 1,
  nodes: [
    { id: 'organization-model', kind: 'organization', label: 'Organization Model' },
    { id: 'system:sales.crm', kind: 'system', label: 'CRM', sourceId: 'sales.crm', systemId: 'sales.crm' },
    {
      id: 'system:sales.lead-gen',
      kind: 'system',
      label: 'Lead Gen',
      sourceId: 'sales.lead-gen',
      systemId: 'sales.lead-gen'
    },
    {
      id: 'ontology:sales.crm:object/deal',
      kind: 'ontology',
      label: 'Deal',
      sourceId: 'sales.crm:object/deal'
    },
    {
      id: 'knowledge:knowledge.playbook-a',
      kind: 'knowledge',
      label: 'Playbook A',
      sourceId: 'knowledge.playbook-a'
    },
    {
      id: 'knowledge:knowledge.strategy-b',
      kind: 'knowledge',
      label: 'Strategy B',
      sourceId: 'knowledge.strategy-b'
    },
    {
      id: 'knowledge:knowledge.reference-c',
      kind: 'knowledge',
      label: 'Reference C',
      sourceId: 'knowledge.reference-c'
    },
    {
      id: 'knowledge:knowledge.playbook-d',
      kind: 'knowledge',
      label: 'Playbook D',
      sourceId: 'knowledge.playbook-d'
    }
  ],
  edges: [
    // contains edges (organization -> knowledge nodes)
    {
      id: 'edge:contains:organization-model:knowledge:knowledge.playbook-a',
      kind: 'contains',
      sourceId: 'organization-model',
      targetId: 'knowledge:knowledge.playbook-a'
    },
    {
      id: 'edge:contains:organization-model:knowledge:knowledge.strategy-b',
      kind: 'contains',
      sourceId: 'organization-model',
      targetId: 'knowledge:knowledge.strategy-b'
    },
    {
      id: 'edge:contains:organization-model:knowledge:knowledge.reference-c',
      kind: 'contains',
      sourceId: 'organization-model',
      targetId: 'knowledge:knowledge.reference-c'
    },
    {
      id: 'edge:contains:organization-model:knowledge:knowledge.playbook-d',
      kind: 'contains',
      sourceId: 'organization-model',
      targetId: 'knowledge:knowledge.playbook-d'
    },
    // governs edges
    {
      id: 'edge:governs:knowledge:knowledge.playbook-a:system:sales.crm',
      kind: 'governs',
      sourceId: 'knowledge:knowledge.playbook-a',
      targetId: 'system:sales.crm'
    },
    {
      id: 'edge:governs:knowledge:knowledge.playbook-a:system:sales.lead-gen',
      kind: 'governs',
      sourceId: 'knowledge:knowledge.playbook-a',
      targetId: 'system:sales.lead-gen'
    },
    {
      id: 'edge:governs:knowledge:knowledge.strategy-b:system:sales.lead-gen',
      kind: 'governs',
      sourceId: 'knowledge:knowledge.strategy-b',
      targetId: 'system:sales.lead-gen'
    },
    {
      id: 'edge:governs:knowledge:knowledge.playbook-d:ontology:sales.crm:object/deal',
      kind: 'governs',
      sourceId: 'knowledge:knowledge.playbook-d',
      targetId: 'ontology:sales.crm:object/deal'
    }
  ]
}

// ---------------------------------------------------------------------------
// bySystem
// ---------------------------------------------------------------------------

describe('bySystem', () => {
  it('returns nodes that have governs edges to the given system', () => {
    const result = bySystem(GRAPH, 'sales.crm', NODES)
    expect(result).toHaveLength(1)
    expect(result[0].id).toBe('knowledge.playbook-a')
  })

  it('returns multiple nodes when multiple knowledge nodes govern the same system', () => {
    const result = bySystem(GRAPH, 'sales.lead-gen', NODES)
    const ids = result.map((n) => n.id).sort()
    expect(ids).toEqual(['knowledge.playbook-a', 'knowledge.strategy-b'].sort())
  })

  it('returns empty array when no node governs the system', () => {
    const result = bySystem(GRAPH, 'system.does-not-exist', NODES)
    expect(result).toHaveLength(0)
  })

  it('returns full OrgKnowledgeNode objects (not just graph stubs)', () => {
    const result = bySystem(GRAPH, 'sales.crm', NODES)
    expect(result[0].body).toBeDefined()
    expect(result[0].links).toBeDefined()
    expect(result[0].ownerIds).toBeDefined()
  })
})

// ---------------------------------------------------------------------------
// byOntology
// ---------------------------------------------------------------------------

describe('byOntology', () => {
  it('returns nodes that have governs edges to the given ontology id', () => {
    const result = byOntology(GRAPH, 'sales.crm:object/deal', NODES)
    expect(result.map((node) => node.id)).toEqual(['knowledge.playbook-d'])
  })

  it('accepts an ontology graph node id', () => {
    const result = byOntology(GRAPH, 'ontology:sales.crm:object/deal', NODES)
    expect(result.map((node) => node.id)).toEqual(['knowledge.playbook-d'])
  })

  it('routes raw ontology ids to ontology graph nodes instead of bridge-era system ids', () => {
    const graph: OrganizationGraph = {
      ...GRAPH,
      nodes: [
        ...GRAPH.nodes,
        {
          id: 'system:sales.crm:object/deal',
          kind: 'system',
          label: 'Bridge-shaped System',
          sourceId: 'sales.crm:object/deal',
          systemId: 'sales.crm:object/deal'
        },
        {
          id: 'knowledge:knowledge.system-shaped',
          kind: 'knowledge',
          label: 'System Shaped',
          sourceId: 'knowledge.system-shaped'
        }
      ],
      edges: [
        ...GRAPH.edges,
        {
          id: 'edge:governs:knowledge:knowledge.system-shaped:system:sales.crm:object/deal',
          kind: 'governs',
          sourceId: 'knowledge:knowledge.system-shaped',
          targetId: 'system:sales.crm:object/deal'
        }
      ]
    }
    const nodes: OrgKnowledgeNode[] = [
      ...NODES,
      {
        id: 'knowledge.system-shaped',
        kind: 'reference',
        title: 'System Shaped',
        summary: 'Should not match raw ontology lookup.',
        body: 'Body.',
        links: [{ nodeId: 'system:sales.crm:object/deal' }],
        ownerIds: [],
        updatedAt: '2026-05-14'
      }
    ]

    expect(byOntology(graph, 'sales.crm:object/deal', nodes).map((node) => node.id)).toEqual(['knowledge.playbook-d'])
  })

  it('is exported from the public knowledge query API', () => {
    expect(publicByOntology).toBe(byOntology)
    expect(publicByOntology(GRAPH, 'sales.crm:object/deal', NODES).map((node) => node.id)).toEqual([
      'knowledge.playbook-d'
    ])
  })
})

// ---------------------------------------------------------------------------
// byKind
// ---------------------------------------------------------------------------

describe('byKind', () => {
  it('returns all playbook nodes', () => {
    const result = byKind(GRAPH, 'playbook', NODES)
    expect(result).toHaveLength(2)
    expect(result.every((n) => n.kind === 'playbook')).toBe(true)
  })

  it('returns strategy nodes', () => {
    const result = byKind(GRAPH, 'strategy', NODES)
    expect(result).toHaveLength(1)
    expect(result[0].id).toBe('knowledge.strategy-b')
  })

  it('returns reference nodes', () => {
    const result = byKind(GRAPH, 'reference', NODES)
    expect(result).toHaveLength(1)
    expect(result[0].id).toBe('knowledge.reference-c')
  })

  it('returns empty array when no nodes match the kind', () => {
    const result = byKind(GRAPH, 'playbook', [])
    expect(result).toHaveLength(0)
  })
})

// ---------------------------------------------------------------------------
// byOwner
// ---------------------------------------------------------------------------

describe('byOwner', () => {
  it('returns nodes where ownerIds includes the given owner', () => {
    const result = byOwner(GRAPH, 'role.ops-lead', NODES)
    const ids = result.map((n) => n.id).sort()
    expect(ids).toEqual(['knowledge.playbook-d', 'knowledge.strategy-b'].sort())
  })

  it('returns nodes for a second owner', () => {
    const result = byOwner(GRAPH, 'role.ceo', NODES)
    expect(result).toHaveLength(1)
    expect(result[0].id).toBe('knowledge.playbook-d')
  })

  it('returns empty array when ownerId is not present in any node', () => {
    const result = byOwner(GRAPH, 'role.unknown', NODES)
    expect(result).toHaveLength(0)
  })
})

// ---------------------------------------------------------------------------
// governs
// ---------------------------------------------------------------------------

describe('governs', () => {
  it('returns target graph node IDs for outgoing governs edges (OM node id input)', () => {
    const result = governs(GRAPH, 'knowledge.playbook-a')
    expect(result.sort()).toEqual(['system:sales.crm', 'system:sales.lead-gen'].sort())
  })

  it('accepts graph node ID format input', () => {
    const result = governs(GRAPH, 'knowledge:knowledge.playbook-a')
    expect(result.sort()).toEqual(['system:sales.crm', 'system:sales.lead-gen'].sort())
  })

  it('returns single target for a node with one link', () => {
    const result = governs(GRAPH, 'knowledge.strategy-b')
    expect(result).toEqual(['system:sales.lead-gen'])
  })

  it('returns empty array for a node with no governs edges', () => {
    const result = governs(GRAPH, 'knowledge.reference-c')
    expect(result).toHaveLength(0)
  })

  it('returns empty array for an unknown nodeId', () => {
    const result = governs(GRAPH, 'knowledge.does-not-exist')
    expect(result).toHaveLength(0)
  })
})

// ---------------------------------------------------------------------------
// governedBy
// ---------------------------------------------------------------------------

describe('governedBy', () => {
  it('returns knowledge graph node IDs for incoming governs edges (prefixed system id)', () => {
    const result = governedBy(GRAPH, 'system:sales.lead-gen')
    const sorted = result.sort()
    expect(sorted).toEqual(['knowledge:knowledge.playbook-a', 'knowledge:knowledge.strategy-b'].sort())
  })

  it('accepts bare system id (auto-prefixes with system:)', () => {
    const result = governedBy(GRAPH, 'sales.crm')
    expect(result).toEqual(['knowledge:knowledge.playbook-a'])
  })

  it('returns empty array when no knowledge node governs the target', () => {
    const result = governedBy(GRAPH, 'system:dashboard')
    expect(result).toHaveLength(0)
  })

  it('returns empty array for unknown target node id', () => {
    const result = governedBy(GRAPH, 'system:does-not-exist')
    expect(result).toHaveLength(0)
  })
})

// ---------------------------------------------------------------------------
// parsePath â€” happy paths (all five mount axes)
// ---------------------------------------------------------------------------

describe('parsePath â€” happy paths', () => {
  it('/by-kind/playbook â†’ mount:by-kind args:[playbook]', () => {
    expect(parsePath('/by-kind/playbook')).toEqual({ mount: 'by-kind', args: ['playbook'] })
  })

  it('/by-kind/strategy â†’ mount:by-kind args:[strategy]', () => {
    expect(parsePath('/by-kind/strategy')).toEqual({ mount: 'by-kind', args: ['strategy'] })
  })

  it('/by-system/sales.crm â†’ mount:by-system args:[sales.crm]', () => {
    expect(parsePath('/by-system/sales.crm')).toEqual({ mount: 'by-system', args: ['sales.crm'] })
  })

  it('/by-system/sales/crm (slash-delimited) â†’ mount:by-system args:[sales/crm]', () => {
    // systemId with slashes is joined and passed as-is for the caller to interpret
    expect(parsePath('/by-system/sales/crm')).toEqual({ mount: 'by-system', args: ['sales/crm'] })
  })

  it('/by-owner/role.ops-lead â†’ mount:by-owner args:[role.ops-lead]', () => {
    expect(parsePath('/by-owner/role.ops-lead')).toEqual({ mount: 'by-owner', args: ['role.ops-lead'] })
  })

  it('/by-ontology/sales.crm:object/deal â†’ mount:by-ontology args:[sales.crm:object/deal]', () => {
    expect(parsePath('/by-ontology/sales.crm:object/deal')).toEqual({
      mount: 'by-ontology',
      args: ['sales.crm:object/deal']
    })
  })

  it('/graph/knowledge.outreach-playbook/governs â†’ mount:graph args:[knowledge.outreach-playbook,governs]', () => {
    expect(parsePath('/graph/knowledge.outreach-playbook/governs')).toEqual({
      mount: 'graph',
      args: ['knowledge.outreach-playbook', 'governs']
    })
  })

  it('/graph/knowledge.outreach-playbook/governed-by â†’ mount:graph args:[knowledge.outreach-playbook,governed-by]', () => {
    expect(parsePath('/graph/knowledge.outreach-playbook/governed-by')).toEqual({
      mount: 'graph',
      args: ['knowledge.outreach-playbook', 'governed-by']
    })
  })

  it('/<nodeId> single segment â†’ mount:node args:[nodeId]', () => {
    expect(parsePath('/knowledge.outreach-playbook')).toEqual({
      mount: 'node',
      args: ['knowledge.outreach-playbook']
    })
  })

  it('trailing slash is stripped before parsing', () => {
    expect(parsePath('/by-kind/playbook/')).toEqual({ mount: 'by-kind', args: ['playbook'] })
  })
})

// ---------------------------------------------------------------------------
// parsePath â€” invalid inputs (5+)
// ---------------------------------------------------------------------------

describe('parsePath â€” invalid inputs', () => {
  it('throws on empty string', () => {
    expect(() => parsePath('')).toThrow('parsePath: path must be a non-empty string')
  })

  it('throws when path does not start with /', () => {
    expect(() => parsePath('by-kind/playbook')).toThrow('parsePath: path must start with "/"')
  })

  it('throws on bare root path "/"', () => {
    expect(() => parsePath('/')).toThrow('parsePath: path resolves to root with no mount')
  })

  it('throws on /by-system with no systemId argument', () => {
    expect(() => parsePath('/by-system')).toThrow('/by-system requires a systemId argument')
  })

  it('throws on /by-kind with no kind argument', () => {
    expect(() => parsePath('/by-kind')).toThrow('/by-kind requires a kind argument')
  })

  it('throws on /by-owner with no ownerId argument', () => {
    expect(() => parsePath('/by-owner')).toThrow('/by-owner requires an ownerId argument')
  })

  it('throws on /graph with only nodeId (missing verb)', () => {
    expect(() => parsePath('/graph/knowledge.outreach-playbook')).toThrow('/graph requires <nodeId>/<verb>')
  })

  it('throws on /graph/<nodeId>/<unknown-verb>', () => {
    expect(() => parsePath('/graph/knowledge.outreach-playbook/list')).toThrow(
      'verb must be "governs" or "governed-by"'
    )
  })

  it('throws on multi-segment path with unrecognized first segment', () => {
    expect(() => parsePath('/unknown-mount/some-arg')).toThrow('unrecognized path pattern')
  })
})

// ---------------------------------------------------------------------------
// parsePath â€” edge cases
// ---------------------------------------------------------------------------

describe('parsePath edge cases', () => {
  // Whitespace handling â€” the parser does NOT trim the incoming string.
  // A path with leading whitespace does not start with '/' so it throws.
  it('throws on path with leading whitespace', () => {
    expect(() => parsePath(' /by-kind/playbook')).toThrow('parsePath: path must start with "/"')
  })

  // Trailing whitespace becomes part of the last segment (not stripped).
  // The segment is non-empty so parsing proceeds and the whitespace is
  // included verbatim in the returned arg.
  it('preserves trailing whitespace inside the last segment', () => {
    const result = parsePath('/by-kind/playbook ')
    expect(result.mount).toBe('by-kind')
    expect(result.args[0]).toBe('playbook ')
  })

  // Multiple trailing slashes are all stripped (regex /\/+$/).
  it('strips multiple trailing slashes', () => {
    expect(parsePath('/by-kind/playbook///')).toEqual({ mount: 'by-kind', args: ['playbook'] })
  })

  // Lone-slash variants â€” a path of only slashes normalises to an empty
  // segments list and must throw the root-with-no-mount error.
  it('throws on path consisting only of slashes', () => {
    expect(() => parsePath('///')).toThrow('parsePath: path resolves to root with no mount')
  })

  // Case sensitivity â€” parsePath does not normalise case.
  // 'Playbook' (capitalised) is passed through as-is to the caller.
  it('preserves node-id case (no case normalisation)', () => {
    // /by-kind takes rest[0] verbatim
    const result = parsePath('/by-kind/Playbook')
    expect(result.args[0]).toBe('Playbook')
  })

  // system id: dot notation passes through unchanged.
  it('/by-system/sales.crm preserves dot notation', () => {
    expect(parsePath('/by-system/sales.crm')).toEqual({ mount: 'by-system', args: ['sales.crm'] })
  })

  // system id: slash notation joins rest segments with '/'.
  // The parser does NOT convert slashes to dots â€” the caller must normalise.
  it('/by-system/sales/crm joins segments with slash (no dot conversion)', () => {
    const result = parsePath('/by-system/sales/crm')
    expect(result.mount).toBe('by-system')
    // args[0] is 'sales/crm', NOT 'sales.crm'
    expect(result.args[0]).toBe('sales/crm')
  })

  // /by-kind silently ignores extra path segments beyond the kind argument.
  it('/by-kind/playbook/extra silently uses only first kind segment', () => {
    // First segment after 'by-kind' is taken; extra is ignored.
    const result = parsePath('/by-kind/playbook/extra')
    expect(result.mount).toBe('by-kind')
    expect(result.args[0]).toBe('playbook')
    expect(result.args).toHaveLength(1)
  })

  // /by-owner joins rest with '/' (same behaviour as /by-system).
  it('/by-owner/role.ops-lead/sub joins segments with slash', () => {
    const result = parsePath('/by-owner/role.ops-lead/sub')
    expect(result.mount).toBe('by-owner')
    expect(result.args[0]).toBe('role.ops-lead/sub')
  })

  // Graph node ID may contain a colon prefix (e.g. knowledge:knowledge.foo).
  // The parser accepts it â€” the rest before the verb is joined with '/'.
  it('/graph/<prefixed-nodeId>/governs round-trips the prefixed id', () => {
    const result = parsePath('/graph/knowledge:knowledge.test-node/governs')
    expect(result.mount).toBe('graph')
    expect(result.args[0]).toBe('knowledge:knowledge.test-node')
    expect(result.args[1]).toBe('governs')
  })

  // /graph with no arguments at all (just '/graph').
  it('throws on /graph with no nodeId or verb', () => {
    expect(() => parsePath('/graph')).toThrow('/graph requires <nodeId>/<verb>')
  })

  // /node mount â€” single-segment path works for any non-reserved first segment.
  it('single-segment non-reserved path is treated as node mount', () => {
    expect(parsePath('/knowledge.my-doc')).toEqual({ mount: 'node', args: ['knowledge.my-doc'] })
  })
})

// ---------------------------------------------------------------------------
// formatText
// ---------------------------------------------------------------------------

describe('formatText', () => {
  it('returns "(no results)" for empty array', () => {
    expect(formatText([])).toBe('(no results)')
  })

  it('includes header row with KIND and ID columns', () => {
    const output = formatText([NODES[0]])
    expect(output).toContain('KIND')
    expect(output).toContain('ID')
    expect(output).toContain('TITLE')
  })

  it('includes node id and title in output', () => {
    const output = formatText([NODES[0]])
    expect(output).toContain('knowledge.playbook-a')
    expect(output).toContain('Playbook A')
  })

  it('truncates long summaries', () => {
    const longSummary = 'x'.repeat(200)
    const node: OrgKnowledgeNode = { ...NODES[0], summary: longSummary }
    const output = formatText([node])
    expect(output).toContain('...')
  })
})

// ---------------------------------------------------------------------------
// formatJson â€” envelope shape
// ---------------------------------------------------------------------------

describe('formatJson', () => {
  it('returns a JSON string with top-level keys: path, mount, args, results', () => {
    const raw = formatJson({
      path: '/by-kind/playbook',
      parsed: { mount: 'by-kind', args: ['playbook'] },
      results: [NODES[0]]
    })
    const parsed = JSON.parse(raw) as Record<string, unknown>
    expect(Object.keys(parsed).sort()).toEqual(['args', 'mount', 'path', 'results'].sort())
  })

  it('preserves path in envelope', () => {
    const raw = formatJson({
      path: '/by-kind/playbook',
      parsed: { mount: 'by-kind', args: ['playbook'] },
      results: []
    })
    expect(JSON.parse(raw)).toMatchObject({ path: '/by-kind/playbook' })
  })

  it('preserves mount in envelope', () => {
    const raw = formatJson({
      path: '/by-kind/strategy',
      parsed: { mount: 'by-kind', args: ['strategy'] },
      results: []
    })
    expect(JSON.parse(raw)).toMatchObject({ mount: 'by-kind' })
  })

  it('preserves args array in envelope', () => {
    const raw = formatJson({
      path: '/by-system/sales.crm',
      parsed: { mount: 'by-system', args: ['sales.crm'] },
      results: []
    })
    expect(JSON.parse(raw)).toMatchObject({ args: ['sales.crm'] })
  })

  it('includes results in envelope for string[] (governs/governedBy)', () => {
    const raw = formatJson({
      path: '/graph/knowledge.playbook-a/governs',
      parsed: { mount: 'graph', args: ['knowledge.playbook-a', 'governs'] },
      results: ['system:sales.crm', 'system:sales.lead-gen']
    })
    const parsed = JSON.parse(raw) as { results: string[] }
    expect(parsed.results).toEqual(['system:sales.crm', 'system:sales.lead-gen'])
  })

  it('envelope is NOT flat { results } â€” must have path/mount/args siblings', () => {
    const raw = formatJson({
      path: '/by-kind/playbook',
      parsed: { mount: 'by-kind', args: ['playbook'] },
      results: [NODES[0]]
    })
    const parsed = JSON.parse(raw) as Record<string, unknown>
    // Must NOT be flat (only results key)
    expect(parsed).toHaveProperty('path')
    expect(parsed).toHaveProperty('mount')
    expect(parsed).toHaveProperty('args')
    expect(parsed).toHaveProperty('results')
  })
})

// ---------------------------------------------------------------------------
// formatIdsOnly
// ---------------------------------------------------------------------------

describe('formatIdsOnly', () => {
  it('returns empty string for empty array', () => {
    expect(formatIdsOnly([])).toBe('')
  })

  it('returns newline-separated ids for OrgKnowledgeNode array', () => {
    const output = formatIdsOnly([NODES[0], NODES[1]])
    const lines = output.split('\n')
    expect(lines).toContain('knowledge.playbook-a')
    expect(lines).toContain('knowledge.strategy-b')
  })

  it('returns newline-separated strings for string[] (governs results)', () => {
    const output = formatIdsOnly(['system:sales.crm', 'system:sales.lead-gen'])
    expect(output).toBe('system:sales.crm\nsystem:sales.lead-gen')
  })
})

// ---------------------------------------------------------------------------
// omSearch — universal keyword search across OM surfaces
// ---------------------------------------------------------------------------

/**
 * Minimal synthetic OrganizationModel for omSearch tests.
 *
 * The omSearch implementation is defensive (uses `?? {}` everywhere), so we
 * only need to populate the surfaces we want to assert on. Cast through
 * `unknown` because we are deliberately omitting domain fields.
 */
const OM_SEARCH_MODEL = {
  knowledge: {
    'knowledge.lead-gen-strategy': {
      id: 'knowledge.lead-gen-strategy',
      kind: 'strategy',
      title: 'Lead Gen Strategy',
      summary: 'ICP scoring and targeting criteria for outbound prospecting.',
      body: '## Lead Gen Strategy\n\nDetailed body about lead gen.',
      links: [],
      ownerIds: ['role.growth-operator'],
      updatedAt: '2026-01-01'
    },
    'knowledge.outreach-playbook': {
      id: 'knowledge.outreach-playbook',
      kind: 'playbook',
      title: 'Outreach Sequence Playbook',
      summary: 'End-to-end cold outreach including warmup and reply handling.',
      body: '## Outreach\n\nBody mentions lead-gen and warmup repeatedly.\nLine 3.\nLine 4.\nLine 5.\nLine 6.\nLine 7.',
      links: [{ nodeId: 'system:sales.lead-gen' }, { nodeId: 'system:sales.crm' }],
      ownerIds: ['role.growth-operator'],
      updatedAt: '2026-02-01'
    }
  },
  systems: {
    sales: {
      id: 'sales',
      order: 10,
      label: 'Sales',
      description: 'Revenue workflows and customer acquisition.',
      kind: 'operational',
      lifecycle: 'active',
      systems: {
        'lead-gen': {
          id: 'sales.lead-gen',
          order: 20,
          label: 'Lead Gen',
          description: 'Prospecting, qualification, and outreach preparation.',
          kind: 'operational',
          parentSystemId: 'sales',
          lifecycle: 'active'
        },
        crm: {
          id: 'sales.crm',
          order: 30,
          label: 'CRM',
          description: 'Customer relationship management and deal pipeline.',
          kind: 'operational',
          parentSystemId: 'sales',
          lifecycle: 'active'
        }
      }
    },
    seo: {
      id: 'seo',
      order: 40,
      label: 'SEO',
      description: 'Search engine optimization and inbound content.',
      kind: 'operational',
      lifecycle: 'active'
    }
  },
  resources: {
    'lgn-apify-discovery-workflow': {
      id: 'lgn-apify-discovery-workflow',
      kind: 'workflow',
      systemPath: 'sales.lead-gen',
      title: 'Apify Discovery Workflow',
      description: 'Discovers prospects via Apify website crawl.',
      status: 'active',
      order: 10,
      codeRefs: []
    },
    'integration-apollo-elevasis': {
      id: 'integration-apollo-elevasis',
      kind: 'integration',
      systemPath: 'sales.lead-gen',
      title: 'Apollo Integration',
      description: 'Apollo contact and company data lookup.',
      provider: 'apollo',
      status: 'active',
      order: 20,
      codeRefs: []
    }
  },
  roles: {
    'role.growth-operator': {
      id: 'role.growth-operator',
      order: 20,
      title: 'Growth Operator',
      responsibilities: ['Own lead generation, prospecting, outreach operations, and acquisition playbooks.'],
      responsibleFor: ['sales.lead-gen']
    },
    'role.crm-operator': {
      id: 'role.crm-operator',
      order: 30,
      title: 'CRM Operator',
      responsibilities: ['Own CRM pipeline workflows, inbound reply handling, and deal-stage actions.'],
      responsibleFor: ['sales.crm']
    }
  },
  policies: {
    'policy.approve-large-deals': {
      id: 'policy.approve-large-deals',
      order: 10,
      label: 'Approve Large Deals',
      description: 'Require approval before sending proposals above $10k.',
      trigger: { kind: 'manual' },
      predicate: { kind: 'always' },
      actions: [{ kind: 'require-approval' }],
      appliesTo: { systemIds: [], actionIds: [], resourceIds: [], roleIds: [] }
    }
  }
} as unknown as OrganizationModel

describe('omSearch', () => {
  it('returns empty array for empty query', () => {
    expect(omSearch(OM_SEARCH_MODEL, '')).toEqual([])
  })

  it('returns empty array for whitespace-only query', () => {
    expect(omSearch(OM_SEARCH_MODEL, '   ')).toEqual([])
  })

  it('matches systems by label and ranks them by score', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'lead gen')
    expect(hits.length).toBeGreaterThan(0)
    const topHit = hits[0]
    expect(topHit.kind).toBe('system')
    expect(topHit.id).toBe('sales.lead-gen')
  })

  it('finds knowledge nodes by title or summary', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'outreach')
    const ids = hits.map((h) => h.id)
    expect(ids).toContain('knowledge.outreach-playbook')
  })

  it('finds resources by title', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'apollo')
    expect(hits.length).toBeGreaterThan(0)
    expect(hits[0].id).toBe('integration-apollo-elevasis')
    expect(hits[0].kind).toBe('resource')
  })

  it('finds roles by responsibilities', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'crm operator')
    const ids = hits.map((h) => h.id)
    expect(ids).toContain('role.crm-operator')
  })

  it('finds policies by label and description', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'approve large deals')
    const ids = hits.map((h) => h.id)
    expect(ids).toContain('policy.approve-large-deals')
  })

  it('is case-insensitive', () => {
    const lower = omSearch(OM_SEARCH_MODEL, 'lead gen').map((h) => h.id)
    const upper = omSearch(OM_SEARCH_MODEL, 'LEAD GEN').map((h) => h.id)
    expect(lower).toEqual(upper)
  })

  it('returns hits ranked by score descending', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'lead gen')
    for (let i = 1; i < hits.length; i++) {
      expect(hits[i - 1].score).toBeGreaterThanOrEqual(hits[i].score)
    }
  })

  it('attaches subKind for typed surfaces', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'apollo')
    expect(hits[0].subKind).toBe('integration')
  })

  it('respects the limit option', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'lead', { limit: 1 })
    expect(hits).toHaveLength(1)
  })

  it('limit: 0 returns all matching hits', () => {
    const limited = omSearch(OM_SEARCH_MODEL, 'lead', { limit: 1 })
    const unlimited = omSearch(OM_SEARCH_MODEL, 'lead', { limit: 0 })
    expect(unlimited.length).toBeGreaterThan(limited.length)
  })

  it('filters by kinds option', () => {
    const only = omSearch(OM_SEARCH_MODEL, 'lead', { kinds: ['system'] })
    expect(only.every((h) => h.kind === 'system')).toBe(true)
  })

  it('returns no hits when the query matches nothing', () => {
    expect(omSearch(OM_SEARCH_MODEL, 'zzz-not-a-real-thing-xyz')).toHaveLength(0)
  })

  it('ranks an exact id match above partial matches', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'sales.lead-gen')
    expect(hits[0].id).toBe('sales.lead-gen')
    expect(hits[0].kind).toBe('system')
  })

  it('ignores single-character tokens (noise filter)', () => {
    // 'a' alone is filtered out by tokenize (length < 2); query collapses to empty
    expect(omSearch(OM_SEARCH_MODEL, 'a')).toEqual([])
  })
})

// ---------------------------------------------------------------------------
// formatOmSearchHits
// ---------------------------------------------------------------------------

describe('formatOmSearchHits', () => {
  it('returns "(no results)" for empty array', () => {
    expect(formatOmSearchHits([])).toBe('(no results)')
  })

  it('includes the kind bracket label for each hit', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'lead gen')
    const output = formatOmSearchHits(hits)
    expect(output).toContain('[system')
  })

  it('includes the id and title for each hit', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'lead gen')
    const output = formatOmSearchHits(hits)
    expect(output).toContain('sales.lead-gen')
    expect(output).toContain('Lead Gen')
  })

  it('appends subKind to the bracket when present', () => {
    const hits = omSearch(OM_SEARCH_MODEL, 'apollo')
    const output = formatOmSearchHits(hits)
    expect(output).toContain('[resource/integration]')
  })
})

// ---------------------------------------------------------------------------
// omDescribe — neighborhood view for any OM node id
// ---------------------------------------------------------------------------

describe('omDescribe', () => {
  it('returns undefined for an unknown id', () => {
    expect(omDescribe(OM_SEARCH_MODEL, 'does-not-exist')).toBeUndefined()
  })

  describe('system', () => {
    it('describes a system with parent, lifecycle, and description', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen')
      expect(result).toBeDefined()
      if (result?.kind !== 'system') throw new Error('expected system')
      expect(result.id).toBe('sales.lead-gen')
      expect(result.label).toBe('Lead Gen')
      expect(result.description).toContain('Prospecting')
      expect(result.parentSystemId).toBe('sales')
      expect(result.lifecycle).toBe('active')
    })

    it('counts resources by kind for the system', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen')
      if (result?.kind !== 'system') throw new Error('expected system')
      expect(result.resourceCountsByKind).toEqual({ workflow: 1, integration: 1 })
      expect(result.resourceIdsByKind.integration).toContain('integration-apollo-elevasis')
    })

    it('finds governing knowledge via knowledge.links', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen')
      if (result?.kind !== 'system') throw new Error('expected system')
      expect(result.governingKnowledgeIds).toContain('knowledge.outreach-playbook')
    })

    it('lists immediate child systems only (not grandchildren)', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'sales')
      if (result?.kind !== 'system') throw new Error('expected system')
      expect(result.childSystemPaths).toEqual(expect.arrayContaining(['sales.lead-gen', 'sales.crm']))
      expect(result.childSystemPaths).toHaveLength(2)
    })
  })

  describe('resource', () => {
    it('describes a workflow resource with system and status', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'lgn-apify-discovery-workflow')
      expect(result).toBeDefined()
      if (result?.kind !== 'resource') throw new Error('expected resource')
      expect(result.resourceKind).toBe('workflow')
      expect(result.systemPath).toBe('sales.lead-gen')
      expect(result.title).toBe('Apify Discovery Workflow')
      expect(result.status).toBe('active')
    })

    it('describes an integration resource', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'integration-apollo-elevasis')
      if (result?.kind !== 'resource') throw new Error('expected resource')
      expect(result.resourceKind).toBe('integration')
    })
  })

  describe('knowledge', () => {
    it('describes a knowledge node with body excerpt and line count', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'knowledge.outreach-playbook')
      expect(result).toBeDefined()
      if (result?.kind !== 'knowledge') throw new Error('expected knowledge')
      expect(result.title).toBe('Outreach Sequence Playbook')
      expect(result.knowledgeKind).toBe('playbook')
      expect(result.bodyLineCount).toBeGreaterThan(0)
      expect(result.bodyExcerpt).toContain('Outreach')
    })

    it('exposes governs targets from links[].nodeId', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'knowledge.outreach-playbook')
      if (result?.kind !== 'knowledge') throw new Error('expected knowledge')
      expect(result.governs).toContain('system:sales.lead-gen')
      expect(result.governs).toContain('system:sales.crm')
    })

    it('exposes ownerIds', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'knowledge.outreach-playbook')
      if (result?.kind !== 'knowledge') throw new Error('expected knowledge')
      expect(result.ownerIds).toContain('role.growth-operator')
    })
  })

  describe('role', () => {
    it('describes a role with responsibilities and responsibleFor systems', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'role.growth-operator')
      expect(result).toBeDefined()
      if (result?.kind !== 'role') throw new Error('expected role')
      expect(result.title).toBe('Growth Operator')
      expect(result.responsibilities.length).toBeGreaterThan(0)
      expect(result.responsibleFor).toContain('sales.lead-gen')
    })
  })

  describe('policy', () => {
    it('describes a policy with trigger kind and effects', () => {
      const result = omDescribe(OM_SEARCH_MODEL, 'policy.approve-large-deals')
      expect(result).toBeDefined()
      if (result?.kind !== 'policy') throw new Error('expected policy')
      expect(result.triggerKind).toBe('manual')
      expect(result.effectKinds).toContain('require-approval')
    })
  })

  describe('kind detection', () => {
    it('detects ontology ids by :<kind>/ pattern', () => {
      // No ontology in fixture, but detection should classify the id correctly.
      // It returns undefined because the record can't be found, but classification reaches the
      // ontology branch (which then fails the lookup).
      expect(omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen:object/list')).toBeUndefined()
    })

    it('returns undefined when an id pattern matches but the entity is missing', () => {
      expect(omDescribe(OM_SEARCH_MODEL, 'knowledge.does-not-exist')).toBeUndefined()
      expect(omDescribe(OM_SEARCH_MODEL, 'role.does-not-exist')).toBeUndefined()
      expect(omDescribe(OM_SEARCH_MODEL, 'policy.does-not-exist')).toBeUndefined()
    })
  })
})

// ---------------------------------------------------------------------------
// formatOmDescribe
// ---------------------------------------------------------------------------

describe('formatOmDescribe', () => {
  it('returns "(node not found)" for undefined', () => {
    expect(formatOmDescribe(undefined)).toBe('(node not found)')
  })

  it('renders system header + sections', () => {
    const result = omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen')
    const output = formatOmDescribe(result)
    expect(output).toContain('System: sales.lead-gen')
    expect(output).toContain('Label: Lead Gen')
    expect(output).toContain('Resources (2)')
  })

  it('renders knowledge with body excerpt header', () => {
    const result = omDescribe(OM_SEARCH_MODEL, 'knowledge.outreach-playbook')
    const output = formatOmDescribe(result)
    expect(output).toContain('Knowledge: knowledge.outreach-playbook')
    expect(output).toContain('Body excerpt')
    expect(output).toContain('use knowledge:cat for full')
  })

  it('renders role with responsibilities', () => {
    const result = omDescribe(OM_SEARCH_MODEL, 'role.growth-operator')
    const output = formatOmDescribe(result)
    expect(output).toContain('Role: role.growth-operator')
    expect(output).toContain('Title: Growth Operator')
    expect(output).toContain('Responsibilities')
  })

  it('renders policy with trigger kind', () => {
    const result = omDescribe(OM_SEARCH_MODEL, 'policy.approve-large-deals')
    const output = formatOmDescribe(result)
    expect(output).toContain('Policy: policy.approve-large-deals')
    expect(output).toContain('Trigger: manual')
  })

  it('renders resource with system path', () => {
    const result = omDescribe(OM_SEARCH_MODEL, 'integration-apollo-elevasis')
    const output = formatOmDescribe(result)
    expect(output).toContain('Resource: integration-apollo-elevasis')
    expect(output).toContain('System: sales.lead-gen')
  })
})

// ---------------------------------------------------------------------------
// Domain-item resolution (item:<domain>:<id> + bare-id back-compat)
// ---------------------------------------------------------------------------

/**
 * Fixture exercising all six graph-projected domain-item kinds.
 * `knowledge.brand-guide` governs the client via a `client:client-1` link.
 */
const DOMAIN_ITEM_MODEL = {
  knowledge: {
    'knowledge.brand-guide': {
      id: 'knowledge.brand-guide',
      kind: 'reference',
      title: 'Brand Guide',
      summary: 'Branding rules for the client.',
      body: '## Brand Guide\n\nVoice and palette.',
      links: [{ nodeId: 'client:client-1' }],
      ownerIds: [],
      updatedAt: '2026-01-01'
    }
  },
  clients: {
    'client-1': {
      id: 'client-1',
      name: 'Byron for Irvine',
      organizationName: 'Byron for Irvine Campaign',
      shortName: 'Byron',
      clientBrief: 'Local city-council campaign.',
      candidateName: 'Byron'
    }
  },
  roles: {
    'role.ops-lead': {
      id: 'role.ops-lead',
      order: 10,
      title: 'Ops Lead',
      responsibilities: ['Run operations.'],
      responsibleFor: []
    }
  },
  policies: {
    'policy.review-spend': {
      id: 'policy.review-spend',
      order: 10,
      label: 'Review Spend',
      description: 'Review spend over threshold.'
    }
  },
  customers: {
    'cust-1': {
      id: 'cust-1',
      name: 'SMB Automation Segment',
      description: 'Small businesses adopting automation.'
    }
  },
  offerings: {
    'off-1': {
      id: 'off-1',
      name: 'Automation Starter Package',
      description: 'Entry automation engagement.'
    }
  },
  goals: {
    'goal-1': {
      id: 'goal-1',
      description: 'Double recurring revenue this year.'
    }
  }
} as unknown as OrganizationModel

describe('omDescribe — domain items', () => {
  it('resolves item:clients:<id> to the full client profile + location + governing knowledge', () => {
    const result = omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:client-1')
    expect(result).toBeDefined()
    if (!result || result.kind !== 'domain-item') throw new Error('expected domain-item result')
    expect(result.domain).toBe('clients')
    expect(result.id).toBe('item:clients:client-1')
    expect(result.name).toBe('Byron for Irvine')
    // Full ClientProfile payload is carried in `record`.
    expect(result.record).toMatchObject({
      organizationName: 'Byron for Irvine Campaign',
      candidateName: 'Byron'
    })
    // Location.
    expect(result.location.domainLabel).toBe('Clients')
    expect(result.location.graphNodeId).toBe('client:client-1')
    expect(result.location.containsParentIds).toContain('organization-model')
    // Governing knowledge.
    expect(result.governingKnowledge.map((g) => g.id)).toContain('knowledge.brand-guide')
  })

  it('resolves bare client:<id> identically to the item: form (back-compat)', () => {
    const bare = omDescribe(DOMAIN_ITEM_MODEL, 'client:client-1')
    const canonical = omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:client-1')
    expect(bare).toEqual(canonical)
  })

  it('resolves the other five domain kinds via item: and bare ids', () => {
    const cases: Array<{ item: string; bare: string; domain: string; name: string }> = [
      { item: 'item:roles:role.ops-lead', bare: 'role:role.ops-lead', domain: 'roles', name: 'Ops Lead' },
      {
        item: 'item:policies:policy.review-spend',
        bare: 'policy:policy.review-spend',
        domain: 'policies',
        name: 'Review Spend'
      },
      { item: 'item:customers:cust-1', bare: 'customer:cust-1', domain: 'customers', name: 'SMB Automation Segment' },
      { item: 'item:offerings:off-1', bare: 'offering:off-1', domain: 'offerings', name: 'Automation Starter Package' },
      { item: 'item:goals:goal-1', bare: 'goal:goal-1', domain: 'goals', name: 'Double recurring revenue this year.' }
    ]
    for (const c of cases) {
      const viaItem = omDescribe(DOMAIN_ITEM_MODEL, c.item)
      expect(viaItem, `item form ${c.item}`).toBeDefined()
      if (!viaItem || viaItem.kind !== 'domain-item') throw new Error(`expected domain-item for ${c.item}`)
      expect(viaItem.domain).toBe(c.domain)
      expect(viaItem.name).toBe(c.name)
      // Bare form resolves to the same result.
      expect(omDescribe(DOMAIN_ITEM_MODEL, c.bare)).toEqual(viaItem)
    }
  })

  it('returns undefined for an unknown item id', () => {
    expect(omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:does-not-exist')).toBeUndefined()
    expect(omDescribe(DOMAIN_ITEM_MODEL, 'client:does-not-exist')).toBeUndefined()
  })

  it('returns undefined for an item ref with an unknown domain', () => {
    expect(omDescribe(DOMAIN_ITEM_MODEL, 'item:widgets:thing-1')).toBeUndefined()
  })
})

describe('parsePath — domain-item mounts', () => {
  it('parses /by-domain/<domain> into a by-domain mount', () => {
    expect(parsePath('/by-domain/clients')).toEqual({ mount: 'by-domain', args: ['clients'] })
  })

  it('parses /by-item/<domain>/<itemId> into a by-item mount', () => {
    expect(parsePath('/by-item/clients/client-1')).toEqual({ mount: 'by-item', args: ['clients', 'client-1'] })
  })

  it('throws on an unknown domain for /by-domain', () => {
    expect(() => parsePath('/by-domain/widgets')).toThrow(/unknown domain/i)
  })

  it('throws on an unknown domain for /by-item', () => {
    expect(() => parsePath('/by-item/widgets/thing-1')).toThrow(/unknown domain/i)
  })

  it('throws when /by-item is missing the itemId', () => {
    expect(() => parsePath('/by-item/clients')).toThrow(/by-item/i)
  })
})

describe('formatOmDescribe — domain items', () => {
  it('renders a domain-item header, location, record, and governing knowledge', () => {
    const result = omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:client-1')
    const output = formatOmDescribe(result)
    expect(output).toContain('Domain Item: item:clients:client-1')
    expect(output).toContain('Domain: Clients')
    expect(output).toContain('Name: Byron for Irvine')
    expect(output).toContain('Graph node: client:client-1')
    expect(output).toContain('Governing knowledge:')
    expect(output).toContain('knowledge.brand-guide')
  })

  it('serializes the domain-item result directly via JSON.stringify', () => {
    const result = omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:client-1')
    const json = JSON.parse(JSON.stringify(result))
    expect(json.kind).toBe('domain-item')
    expect(json.domain).toBe('clients')
    expect(json.id).toBe('item:clients:client-1')
  })
})
