import { describe, expect, it } from 'vitest'
import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
import { resolveSystemConfig } from '../helpers'
import {
  compileOrganizationOntology,
  formatOntologyId,
  listResolvedOntologyRecords,
  parseOntologyId
} from '../ontology'
import { SYSTEM_INTERFACE_PROFILES } from '../domains/systems'
import { resolveOrganizationModelWithResources } from '../resolve'
import { OrganizationModelSchema } from '../schema'

// Phase 4 (D8): sales, prospecting, projects, navigation top-level fields removed.
// DEFAULT_ORGANIZATION_MODEL_SALES / _PROSPECTING / _PROJECTS / OrganizationModelSalesSchema etc.
// no longer exported. makeMinimalModel no longer includes those fields.
// knowledge is now a flat Record<id, OrgKnowledgeNode> (D3) — wrapper shape removed.

function makeSystem(id: string, path = `/${id.replaceAll('.', '/')}`) {
  return {
    id,
    order: 10,
    label: id,
    enabled: true,
    lifecycle: 'active' as const,
    path
  }
}

function makeMinimalModel(systems: Record<string, unknown> = {}) {
  const entityOwnerId = Object.keys(systems)[0] ?? 'dashboard'
  return {
    version: 1 as const,
    branding: DEFAULT_ORGANIZATION_MODEL_BRANDING,
    // Phase 4: sales, prospecting, projects removed from top-level OM fields.
    entities: {
      'crm.deal': { id: 'crm.deal', order: 10, label: 'Deal', ownedBySystemId: entityOwnerId },
      'leadgen.list': { id: 'leadgen.list', order: 20, label: 'Lead List', ownedBySystemId: entityOwnerId },
      'leadgen.company': { id: 'leadgen.company', order: 30, label: 'Lead Company', ownedBySystemId: entityOwnerId },
      'leadgen.contact': { id: 'leadgen.contact', order: 40, label: 'Lead Contact', ownedBySystemId: entityOwnerId },
      'delivery.project': { id: 'delivery.project', order: 50, label: 'Project', ownedBySystemId: entityOwnerId },
      'delivery.milestone': { id: 'delivery.milestone', order: 60, label: 'Milestone', ownedBySystemId: entityOwnerId },
      'delivery.task': { id: 'delivery.task', order: 70, label: 'Task', ownedBySystemId: entityOwnerId }
    },
    systems
  }
}

function getIssueMessages(data: unknown): string[] {
  const result = OrganizationModelSchema.safeParse(data)
  if (result.success) return []
  return result.error.issues.map((issue) => issue.message)
}

describe('system tree validation', () => {
  it('passes with a flat list that has declared ancestors', () => {
    const model = makeMinimalModel({
      sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
      'sales.crm': makeSystem('sales.crm', '/sales/crm/pipeline'),
      'sales.lead-gen': makeSystem('sales.lead-gen', '/lead-gen/lists')
    })

    expect(() => OrganizationModelSchema.parse(model)).not.toThrow()
  })

  it('rejects duplicate system ids', () => {
    // In a record, duplicate keys are impossible at the JS level.
    // The schema refine checks that entry.id === mapKey, so mismatched ids are caught.
    const messages = getIssueMessages(
      makeMinimalModel({
        sales: { id: 'NOT_sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' }
      })
    )

    expect(messages.some((message) => message.includes('Each system entry id must match its map key'))).toBe(true)
  })

  it('rejects duplicate effective system paths when explicit and default paths collide', () => {
    const messages = getIssueMessages(
      makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
        'sales.crm': makeSystem('sales.crm', '/sales/lead-gen'),
        'sales.lead-gen': { id: 'sales.lead-gen', order: 20, label: 'Lead Gen', enabled: true, lifecycle: 'active' }
      })
    )

    expect(
      messages.some((message) =>
        message.includes('System "sales.lead-gen" effective path "/sales/lead-gen" duplicates system "sales.crm"')
      )
    ).toBe(true)
  })

  it('rejects a dotted child when its immediate parent is missing', () => {
    const messages = getIssueMessages(
      makeMinimalModel({
        'sales.crm.pipeline': { ...makeSystem('sales.crm.pipeline', '/pipeline'), parentSystemId: 'sales.crm' }
      })
    )

    expect(messages.some((message) => message.includes('unknown parent "sales.crm"'))).toBe(true)
  })

  it('allows a leaf without a path so resolvers can derive a default path', () => {
    const result = OrganizationModelSchema.safeParse(
      makeMinimalModel({ sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' } })
    )

    expect(result.success).toBe(true)
  })

  it('rejects an active container with no active descendants', () => {
    const messages = getIssueMessages(
      makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
        'sales.crm': {
          id: 'sales.crm',
          order: 20,
          label: 'CRM',
          enabled: false,
          lifecycle: 'deprecated',
          path: '/sales/crm/pipeline'
        }
      })
    )

    expect(messages.some((message) => message.includes('has no active descendants'))).toBe(true)
  })

  it('navigation.sidebar defaults to empty sections when omitted', () => {
    const model = OrganizationModelSchema.parse(makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }))

    expect(model.navigation.sidebar.primary).toEqual({})
    expect(model.navigation.sidebar.bottom).toEqual({})
    expect('surfaces' in model).toBe(false)
    expect('navigationGroups' in model).toBe(false)
    expect('resourceMappings' in model).toBe(false)
  })

  // Phase 4: the old model.navigation.defaultSurfaceId validation no longer exists.
  // Navigation defaults are now managed at the foundation layer (createFoundationOrganizationModel).
  // The following tests that exercised model.navigation.* refines are skipped with a reason.
  it.skip('rejects an unknown navigation default surface when surfaces are explicitly empty (deferred — Phase 4: navigation field removed)', () => {
    // Previously tested: model.navigation.defaultSurfaceId referencing a missing surface.
    // The navigation domain no longer exists at the top-level OM schema level.
  })

  it.skip('rejects navigation group surfaceIds that reference missing surfaces (deferred — Phase 4: navigation field removed)', () => {
    // Previously tested: model.navigation.groups[*].surfaceIds referencing missing surfaces.
    // Navigation groups are now top-level navigationGroups Record — cross-ref validation
    // is handled at the resolveOrganizationModel refine layer if/when re-added.
  })

  it.skip('rejects navigation surface systemIds references to missing systems (deferred — Phase 4: navigation field removed)', () => {
    // Previously tested: model.navigation.surfaces[*].systemIds referencing missing systems.
    // Surfaces are now top-level surfaces Record — cross-ref validation is handled separately.
  })

  it.skip('accepts navigation systemIds when canonical systems exist (deferred — Phase 4: navigation field removed)', () => {
    // Previously tested: model.navigation.surfaces[*].systemIds with valid system refs.
    // See navigation.test.ts for top-level surfaces/navigationGroups coverage.
  })
})

describe('domain metadata validation', () => {
  it('defaults domain metadata and knowledge domain versioning', () => {
    const model = OrganizationModelSchema.parse(makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }))

    expect(model.domainMetadata.knowledge).toEqual({ version: 1, lastModified: '2026-05-10' })
    expect(model.domainMetadata.actions).toEqual({ version: 1, lastModified: '2026-05-10' })
    expect(model.domainMetadata.entities).toEqual({ version: 1, lastModified: '2026-05-10' })
    expect(model.domainMetadata.policies).toEqual({ version: 1, lastModified: '2026-05-10' })
    // Phase 4 (D7): knowledge version/lastModified moved to domainMetadata.knowledge.
    // model.knowledge is now a flat Record<id, OrgKnowledgeNode> — no .version or .lastModified.
    expect(model.domainMetadata.knowledge.version).toBe(1)
    expect(model.domainMetadata.knowledge.lastModified).toBe('2026-05-10')
  })

  it('rejects malformed domain lastModified dates', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }),
      domainMetadata: {
        knowledge: { version: 1, lastModified: '05/10/2026' }
      }
    })

    expect(messages.some((message) => message.includes('lastModified must be an ISO date string'))).toBe(true)
  })
})

describe('retired contract authoring surfaces', () => {
  it('does not materialize absent System Contracts or Bridge Contracts for existing OMs', () => {
    const model = OrganizationModelSchema.parse(makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }))

    expect((model.systems.dashboard as Record<string, unknown>).contracts).toBeUndefined()
    expect((model as Record<string, unknown>).bridgeContracts).toBeUndefined()
  })

  it('rejects authored System Contracts and top-level Bridge Contracts', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', lifecycle: 'active' },
        'sales.lead-gen': {
          ...makeSystem('sales.lead-gen', '/lead-gen/lists'),
          contracts: [
            {
              id: 'retired.contract',
              ownerSystemPath: 'sales.lead-gen',
              provided: []
            }
          ]
        },
        'sales.crm': {
          ...makeSystem('sales.crm', '/crm'),
          contracts: [
            {
              id: 'retired.crm.contract',
              ownerSystemPath: 'sales.crm',
              provided: []
            }
          ]
        }
      }),
      bridgeContracts: [
        {
          id: 'retired.bridge',
          ownerSystemPath: 'sales.lead-gen',
          consumerSystemPath: 'sales.crm',
          required: [],
          provided: []
        }
      ]
    })

    expect(messages.some((message) => message.includes('Unrecognized key'))).toBe(true)
  })
})

describe('System Interface schema integration', () => {
  it('accepts every registry-backed System Interface readiness profile', () => {
    for (const profile of SYSTEM_INTERFACE_PROFILES) {
      const result = OrganizationModelSchema.safeParse({
        ...makeMinimalModel({
          sales: {
            id: 'sales',
            order: 10,
            label: 'Sales',
            lifecycle: 'active'
          },
          [profile.systemPath]: {
            ...makeSystem(profile.systemPath, `/${profile.systemPath.replaceAll('.', '/')}`),
            apiInterface: {
              lifecycle: 'active',
              readinessProfile: profile.readinessProfile
            }
          }
        })
      })

      expect(result.success).toBe(true)
    }
  })

  it('rejects unsupported System Interface readiness profiles', () => {
    const messages = getIssueMessages(
      makeMinimalModel({
        sales: {
          id: 'sales',
          order: 10,
          label: 'Sales',
          lifecycle: 'active'
        },
        'sales.lead-gen': {
          ...makeSystem('sales.lead-gen', '/lead-gen/lists'),
          apiInterface: {
            lifecycle: 'active',
            readinessProfile: 'sales.lead-gen.unknown'
          }
        }
      })
    )

    expect(messages.some((message) => message.includes('Invalid option'))).toBe(true)
  })

  it('accepts a System API Interface scoped to resources from the same System', () => {
    const result = OrganizationModelSchema.safeParse({
      ...makeMinimalModel({
        'sales.lead-gen': {
          ...makeSystem('sales.lead-gen', '/lead-gen/lists'),
          apiInterface: {
            lifecycle: 'active',
            readinessProfile: 'sales.lead-gen.api',
            resourceIds: ['lead-gen-list-builder']
          }
        }
      }),
      resources: {
        'lead-gen-list-builder': {
          id: 'lead-gen-list-builder',
          order: 10,
          kind: 'workflow',
          systemPath: 'sales.lead-gen',
          status: 'active'
        }
      }
    })

    expect(result.success).toBe(true)
    if (result.success) {
      expect(result.data.systems['sales.lead-gen']?.apiInterface).toMatchObject({
        lifecycle: 'active',
        readinessProfile: 'sales.lead-gen.api',
        resourceIds: ['lead-gen-list-builder']
      })
    }
  })

  it('rejects System Interface resource scopes that point at missing or foreign-System resources', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        'sales.lead-gen': {
          ...makeSystem('sales.lead-gen', '/lead-gen/lists'),
          apiInterface: {
            readinessProfile: 'sales.lead-gen.api',
            resourceIds: ['missing-resource', 'crm-deal-sync']
          }
        },
        'sales.crm': makeSystem('sales.crm', '/crm')
      }),
      resources: {
        'crm-deal-sync': {
          id: 'crm-deal-sync',
          order: 10,
          kind: 'workflow',
          systemPath: 'sales.crm',
          status: 'active'
        }
      }
    })

    expect(messages.some((message) => message.includes('scopes unknown resource "missing-resource"'))).toBe(true)
    expect(
      messages.some((message) => message.includes('scopes resource "crm-deal-sync" from system "sales.crm"'))
    ).toBe(true)
  })
})

describe('deployment projection validation', () => {
  it('rejects old deploy governance projections that omit referenced full-model domains', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        sales: {
          ...makeSystem('sales', '/sales'),
          responsibleRoleId: 'role-ops-lead',
          drivesGoals: ['goal-qualified-pipeline-q2-2026']
        }
      }),
      resources: {
        'lead-gen-workflow': {
          id: 'lead-gen-workflow',
          order: 10,
          kind: 'workflow',
          systemPath: 'sales',
          status: 'active',
          ownerRoleId: 'role-ops-lead'
        }
      }
    })

    expect(messages.some((message) => message.includes('unknown responsibleRoleId "role-ops-lead"'))).toBe(true)
    expect(messages.some((message) => message.includes('unknown goal "goal-qualified-pipeline-q2-2026"'))).toBe(true)
    expect(messages.some((message) => message.includes('unknown ownerRoleId "role-ops-lead"'))).toBe(true)
  })
})

describe('ontology contract validation', () => {
  it('parses and formats scoped and global ontology ids', () => {
    expect(parseOntologyId('sales.crm:object/deal')).toEqual({
      id: 'sales.crm:object/deal',
      scope: 'sales.crm',
      kind: 'object',
      localId: 'deal',
      isGlobal: false
    })
    expect(parseOntologyId('global:value-type/email')).toMatchObject({
      scope: 'global',
      kind: 'value-type',
      localId: 'email',
      isGlobal: true
    })
    expect(
      formatOntologyId({ scope: 'enterprise.revenue.sales.crm', kind: 'action', localId: 'deal.update-stage' })
    ).toBe('enterprise.revenue.sales.crm:action/deal.update-stage')
    expect(() => parseOntologyId('sales.crm/object/deal')).toThrow()
    expect(() => parseOntologyId('sales.crm:unknown/deal')).toThrow()
  })

  it('accepts ontology scopes at the top level and system level', () => {
    const result = OrganizationModelSchema.safeParse({
      ...makeMinimalModel({
        sales: {
          ...makeSystem('sales', '/sales'),
          ontology: {
            catalogTypes: {
              'sales:catalog/pipeline': {
                id: 'sales:catalog/pipeline',
                label: 'Pipeline',
                kind: 'pipeline',
                ownerSystemId: 'sales'
              }
            }
          }
        }
      }),
      ontology: {
        valueTypes: {
          'global:value-type/email': {
            id: 'global:value-type/email',
            label: 'Email',
            primitive: 'string'
          }
        }
      }
    })

    expect(result.success).toBe(true)
  })

  it('reports invalid and wrong-scope ontology ids with diagnostic context', () => {
    const compilation = compileOrganizationOntology({
      ontology: {
        objectTypes: {
          'dashboard:object/deal': {
            id: 'Bad Ontology ID',
            label: 'Bad ID'
          },
          'dashboard:action/send-reply': {
            id: 'dashboard:action/send-reply',
            label: 'Misfiled Action'
          }
        }
      }
    })

    expect(compilation.diagnostics).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          code: 'invalid_ontology_id',
          id: 'Bad Ontology ID',
          source: 'organization.ontology',
          path: ['ontology', 'objectTypes', 'dashboard:object/deal'],
          origin: expect.objectContaining({ kind: 'authored', source: 'organization.ontology' })
        }),
        expect.objectContaining({
          code: 'ontology_kind_mismatch',
          id: 'dashboard:action/send-reply',
          source: 'organization.ontology',
          path: ['ontology', 'objectTypes', 'dashboard:action/send-reply'],
          origin: expect.objectContaining({ kind: 'authored', source: 'organization.ontology' })
        })
      ])
    )
  })

  it('returns deterministic compiled ontology indexes and record lists', () => {
    const compilation = compileOrganizationOntology({
      ontology: {
        objectTypes: {
          'dashboard:object/z-last': {
            id: 'dashboard:object/z-last',
            label: 'Z Last'
          },
          'dashboard:object/a-first': {
            id: 'dashboard:object/a-first',
            label: 'A First'
          }
        },
        actionTypes: {
          'dashboard:action/z-last': {
            id: 'dashboard:action/z-last',
            label: 'Z Last'
          },
          'dashboard:action/a-first': {
            id: 'dashboard:action/a-first',
            label: 'A First'
          }
        }
      }
    })

    expect(Object.keys(compilation.ontology.objectTypes)).toEqual([
      'dashboard:object/a-first',
      'dashboard:object/z-last'
    ])
    expect(Object.keys(compilation.ontology.actionTypes)).toEqual([
      'dashboard:action/a-first',
      'dashboard:action/z-last'
    ])
    expect(listResolvedOntologyRecords(compilation.ontology).map((entry) => entry.id)).toEqual([
      'dashboard:object/a-first',
      'dashboard:object/z-last',
      'dashboard:action/a-first',
      'dashboard:action/z-last'
    ])
  })

  it('projects legacy entities and actions while reading catalogs from authored ontology', () => {
    const model = OrganizationModelSchema.parse({
      ...makeMinimalModel({
        dashboard: {
          ...makeSystem('dashboard', '/'),
          ontology: {
            catalogTypes: {
              'dashboard:catalog/pipeline': {
                id: 'dashboard:catalog/pipeline',
                kind: 'pipeline',
                appliesTo: 'dashboard:object/crm.deal',
                label: 'Pipeline',
                entries: {
                  open: { label: 'Open', order: 10, semanticClass: 'open' }
                }
              }
            }
          }
        }
      }),
      actions: {
        send_reply: {
          id: 'send_reply',
          order: 10,
          label: 'Send Reply',
          affects: ['crm.deal']
        }
      }
    })

    const compilation = compileOrganizationOntology(model)

    expect(compilation.diagnostics).toEqual([])
    expect(compilation.ontology.objectTypes['dashboard:object/crm.deal']).not.toHaveProperty('legacyEntityId')
    expect(compilation.ontology.objectTypes['dashboard:object/crm.deal']?.origin).toMatchObject({
      source: 'legacy.entities',
      legacyId: 'crm.deal'
    })
    expect(compilation.ontology.actionTypes['dashboard:action/send_reply']?.legacyActionId).toBe('send_reply')
    expect(compilation.ontology.catalogTypes['dashboard:catalog/pipeline']?.origin).toMatchObject({
      source: 'system:dashboard.ontology',
      systemPath: 'dashboard'
    })
  })

  it('adds origin metadata to authored compiled ontology records without mutating source', () => {
    const authoredObject = {
      id: 'dashboard:object/task',
      label: 'Task',
      ownerSystemId: 'dashboard'
    }
    const authoredCatalog = {
      id: 'dashboard:catalog/task-status',
      label: 'Task Status',
      kind: 'status-flow',
      appliesTo: 'dashboard:object/task',
      entries: {
        planned: { label: 'Planned', order: 10 }
      }
    }
    const model = OrganizationModelSchema.parse({
      ...makeMinimalModel({
        dashboard: {
          ...makeSystem('dashboard', '/'),
          ontology: {
            objectTypes: {
              'dashboard:object/task': authoredObject
            },
            catalogTypes: {
              'dashboard:catalog/task-status': authoredCatalog
            }
          }
        }
      })
    })

    const compilation = compileOrganizationOntology(model)

    expect(compilation.ontology.objectTypes['dashboard:object/task']?.origin).toMatchObject({
      kind: 'authored',
      source: 'system:dashboard.ontology',
      systemPath: 'dashboard'
    })
    expect(compilation.ontology.catalogTypes['dashboard:catalog/task-status']?.origin).toMatchObject({
      kind: 'authored',
      source: 'system:dashboard.ontology',
      systemPath: 'dashboard'
    })
    expect(authoredObject).not.toHaveProperty('origin')
    expect(authoredCatalog).not.toHaveProperty('origin')
    expect(model.systems.dashboard?.ontology?.objectTypes?.['dashboard:object/task']).not.toHaveProperty('origin')
  })

  it('rejects duplicate ontology ids with source context', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        dashboard: makeSystem('dashboard', '/')
      }),
      ontology: {
        objectTypes: {
          'dashboard:object/crm.deal': {
            id: 'dashboard:object/crm.deal',
            label: 'Duplicate Deal',
            ownerSystemId: 'dashboard'
          }
        }
      }
    })

    expect(messages.some((message) => message.includes('Duplicate ontology ID "dashboard:object/crm.deal"'))).toBe(true)
    expect(messages.some((message) => message.includes('legacy.entities'))).toBe(true)
  })

  it('keeps duplicate diagnostics origin-aware for authored versus projected records', () => {
    const model = OrganizationModelSchema.parse(
      makeMinimalModel({
        dashboard: makeSystem('dashboard', '/')
      })
    )
    const compilation = compileOrganizationOntology({
      ...model,
      ontology: {
        objectTypes: {
          'dashboard:object/crm.deal': {
            id: 'dashboard:object/crm.deal',
            label: 'Duplicate Deal',
            ownerSystemId: 'dashboard'
          }
        }
      }
    })

    expect(compilation.diagnostics[0]).toMatchObject({
      code: 'duplicate_ontology_id',
      origin: { kind: 'projected', source: 'legacy.entities', legacyId: 'crm.deal' },
      existingOrigin: { kind: 'authored', source: 'organization.ontology' }
    })
  })

  it('accepts resource ontology bindings when referenced ontology records exist', () => {
    const result = OrganizationModelSchema.safeParse({
      ...makeMinimalModel({
        dashboard: {
          ...makeSystem('dashboard', '/'),
          ontology: {
            objectTypes: {
              'dashboard:object/task': {
                id: 'dashboard:object/task',
                label: 'Task',
                ownerSystemId: 'dashboard'
              }
            },
            actionTypes: {
              'dashboard:action/process-task': {
                id: 'dashboard:action/process-task',
                label: 'Process Task',
                ownerSystemId: 'dashboard',
                actsOn: ['dashboard:object/task']
              }
            },
            catalogTypes: {
              'dashboard:catalog/task-status': {
                id: 'dashboard:catalog/task-status',
                label: 'Task Status',
                ownerSystemId: 'dashboard',
                appliesTo: 'dashboard:object/task'
              }
            },
            eventTypes: {
              'dashboard:event/task-processed': {
                id: 'dashboard:event/task-processed',
                label: 'Task Processed',
                ownerSystemId: 'dashboard'
              }
            }
          }
        }
      }),
      resources: {
        'task-processor': {
          id: 'task-processor',
          order: 10,
          kind: 'workflow',
          systemPath: 'dashboard',
          status: 'active',
          ontology: {
            actions: ['dashboard:action/process-task'],
            primaryAction: 'dashboard:action/process-task',
            reads: ['dashboard:object/task'],
            writes: ['dashboard:object/task'],
            usesCatalogs: ['dashboard:catalog/task-status'],
            emits: ['dashboard:event/task-processed']
          }
        }
      }
    })

    expect(result.success).toBe(true)
  })

  it('accepts known ontology references inside loose property, input, effect, entry, and payload fields', () => {
    const result = OrganizationModelSchema.safeParse({
      ...makeMinimalModel({
        dashboard: {
          ...makeSystem('dashboard', '/'),
          ontology: {
            objectTypes: {
              'dashboard:object/task': {
                id: 'dashboard:object/task',
                label: 'Task',
                ownerSystemId: 'dashboard',
                properties: {
                  id: { valueType: 'global:value-type/uuid' },
                  status: { catalogType: 'dashboard:catalog/task-status' }
                }
              }
            },
            actionTypes: {
              'dashboard:action/process-task': {
                id: 'dashboard:action/process-task',
                label: 'Process Task',
                ownerSystemId: 'dashboard',
                actsOn: ['dashboard:object/task'],
                input: {
                  status: { catalogType: 'dashboard:catalog/task-status' }
                },
                effects: [
                  { kind: 'write', objectType: 'dashboard:object/task' },
                  { kind: 'emitEvent', eventType: 'dashboard:event/task-processed' }
                ]
              }
            },
            catalogTypes: {
              'dashboard:catalog/task-status': {
                id: 'dashboard:catalog/task-status',
                label: 'Task Status',
                ownerSystemId: 'dashboard',
                appliesTo: 'dashboard:object/task',
                entries: {
                  todo: { label: 'To Do' },
                  done: { label: 'Done', stepCatalog: 'dashboard:catalog/task-status' }
                }
              }
            },
            eventTypes: {
              'dashboard:event/task-processed': {
                id: 'dashboard:event/task-processed',
                label: 'Task Processed',
                ownerSystemId: 'dashboard',
                payload: {
                  taskId: { valueType: 'global:value-type/uuid' },
                  status: { catalogType: 'dashboard:catalog/task-status' }
                }
              }
            }
          }
        }
      })
    })

    expect(result.success).toBe(true)
  })

  it('rejects known ontology reference keys that point at missing records', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        dashboard: {
          ...makeSystem('dashboard', '/'),
          ontology: {
            objectTypes: {
              'dashboard:object/task': {
                id: 'dashboard:object/task',
                label: 'Task',
                ownerSystemId: 'dashboard',
                properties: {
                  badValue: { valueType: 'global:value-type/missing' },
                  badCatalog: { catalogType: 'dashboard:catalog/missing' }
                }
              }
            },
            actionTypes: {
              'dashboard:action/process-task': {
                id: 'dashboard:action/process-task',
                label: 'Process Task',
                ownerSystemId: 'dashboard',
                effects: [{ kind: 'emitEvent', eventType: 'dashboard:event/missing' }]
              }
            }
          }
        }
      })
    })

    expect(messages.some((message) => message.includes('valueType references unknown value-type ontology ID'))).toBe(
      true
    )
    expect(messages.some((message) => message.includes('catalogType references unknown catalog ontology ID'))).toBe(
      true
    )
    expect(messages.some((message) => message.includes('eventType references unknown event ontology ID'))).toBe(true)
  })

  it('rejects resource ontology bindings to missing compiled ontology records', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        dashboard: makeSystem('dashboard', '/')
      }),
      resources: {
        'task-processor': {
          id: 'task-processor',
          order: 10,
          kind: 'workflow',
          systemPath: 'dashboard',
          status: 'active',
          ontology: {
            actions: ['dashboard:action/missing-task'],
            primaryAction: 'dashboard:action/missing-task',
            reads: ['dashboard:object/missing-task'],
            usesCatalogs: ['dashboard:catalog/missing-status'],
            emits: ['dashboard:event/missing-event']
          }
        }
      }
    })

    expect(messages.some((message) => message.includes('references unknown action ontology ID'))).toBe(true)
    expect(messages.some((message) => message.includes('references unknown object ontology ID'))).toBe(true)
    expect(messages.some((message) => message.includes('references unknown catalog ontology ID'))).toBe(true)
    expect(messages.some((message) => message.includes('references unknown event ontology ID'))).toBe(true)
  })
})

describe('system config contract', () => {
  it('accepts authored system-local JSON config', () => {
    const model = OrganizationModelSchema.parse(
      makeMinimalModel({
        dashboard: {
          ...makeSystem('dashboard', '/'),
          config: {
            defaultPipelineId: 'sales.crm:catalog/pipeline',
            kanban: {
              groupBy: 'stage',
              showProbability: true
            },
            retryDelays: [100, 500, null]
          }
        }
      })
    )

    expect(model.systems.dashboard?.config).toEqual({
      defaultPipelineId: 'sales.crm:catalog/pipeline',
      kanban: {
        groupBy: 'stage',
        showProbability: true
      },
      retryDelays: [100, 500, null]
    })
  })

  it('rejects non-JSON system config values', () => {
    const result = OrganizationModelSchema.safeParse(
      makeMinimalModel({
        dashboard: {
          ...makeSystem('dashboard', '/'),
          config: {
            callback: () => true
          }
        }
      })
    )

    expect(result.success).toBe(false)
  })

  it('resolves first-class system config without bridge-era config:kv content', () => {
    const model = OrganizationModelSchema.parse(
      makeMinimalModel({
        dashboard: {
          ...makeSystem('dashboard', '/'),
          config: {
            retries: 3,
            mode: 'direct',
            nested: { direct: true, enabled: true }
          }
        }
      })
    )

    expect(resolveSystemConfig(model, 'dashboard')).toEqual({
      retries: 3,
      mode: 'direct',
      nested: { direct: true, enabled: true }
    })

    const resolved = resolveOrganizationModelWithResources({
      systems: {
        dashboard: {
          ...makeSystem('dashboard', '/'),
          config: { enabled: true, retries: 3 }
        }
      }
    })
    expect(resolved.systems.dashboard?.config).toEqual({ enabled: true, retries: 3 })
  })
})

describe('entity validation', () => {
  it('rejects entities owned by unknown systems', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }),
      entities: {
        'crm.deal': { id: 'crm.deal', order: 10, label: 'Deal', ownedBySystemId: 'missing.system' },
        'leadgen.list': { id: 'leadgen.list', order: 20, label: 'Lead List', ownedBySystemId: 'dashboard' },
        'leadgen.company': { id: 'leadgen.company', order: 30, label: 'Lead Company', ownedBySystemId: 'dashboard' },
        'leadgen.contact': { id: 'leadgen.contact', order: 40, label: 'Lead Contact', ownedBySystemId: 'dashboard' },
        'delivery.project': { id: 'delivery.project', order: 50, label: 'Project', ownedBySystemId: 'dashboard' },
        'delivery.milestone': { id: 'delivery.milestone', order: 60, label: 'Milestone', ownedBySystemId: 'dashboard' },
        'delivery.task': { id: 'delivery.task', order: 70, label: 'Task', ownedBySystemId: 'dashboard' }
      }
    })

    expect(messages.some((message) => message.includes('references unknown ownedBySystemId "missing.system"'))).toBe(
      true
    )
  })

  it('rejects entity links to unknown entities', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }),
      entities: {
        'crm.deal': {
          id: 'crm.deal',
          order: 10,
          label: 'Deal',
          ownedBySystemId: 'dashboard',
          links: [{ toEntity: 'missing.entity', kind: 'belongs-to' }]
        },
        'leadgen.list': { id: 'leadgen.list', order: 20, label: 'Lead List', ownedBySystemId: 'dashboard' },
        'leadgen.company': { id: 'leadgen.company', order: 30, label: 'Lead Company', ownedBySystemId: 'dashboard' },
        'leadgen.contact': { id: 'leadgen.contact', order: 40, label: 'Lead Contact', ownedBySystemId: 'dashboard' },
        'delivery.project': { id: 'delivery.project', order: 50, label: 'Project', ownedBySystemId: 'dashboard' },
        'delivery.milestone': { id: 'delivery.milestone', order: 60, label: 'Milestone', ownedBySystemId: 'dashboard' },
        'delivery.task': { id: 'delivery.task', order: 70, label: 'Task', ownedBySystemId: 'dashboard' }
      }
    })

    expect(messages.some((message) => message.includes('links to unknown entity "missing.entity"'))).toBe(true)
  })

  // Phase 4: Prospecting.contactEntityId validation removed (prospecting domain deleted).
  // The separate test for prospecting entity refs is skipped below.
  it.skip('rejects prospecting contactEntityId referencing unknown entity (deferred — Phase 4: prospecting domain removed)', () => {
    // Previously tested: model.prospecting.contactEntityId referencing a missing entity.
    // The prospecting domain no longer exists at the top-level OM schema level.
  })
})

describe('system action and policy validation', () => {
  it('accepts systems that attach known actions and policies', () => {
    const result = OrganizationModelSchema.safeParse({
      ...makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
        'sales.lead-gen': makeSystem('sales.lead-gen')
      }),
      systems: {
        sales: {
          id: 'sales',
          order: 10,
          label: 'Sales',
          lifecycle: 'active'
        },
        'sales.lead-gen': {
          id: 'sales.lead-gen',
          order: 20,
          label: 'Lead Gen',
          lifecycle: 'active',
          actions: [{ actionId: 'lead-gen.company.source', intent: 'exposes' }],
          policies: ['policy.lead-gen.approval']
        }
      },
      actions: {
        'lead-gen.company.source': {
          id: 'lead-gen.company.source',
          order: 10,
          label: 'Source companies'
        }
      },
      policies: {
        'policy.lead-gen.approval': {
          id: 'policy.lead-gen.approval',
          order: 10,
          label: 'Lead Gen Approval',
          trigger: { kind: 'manual' },
          actions: [{ kind: 'block' }],
          appliesTo: { systemIds: ['sales.lead-gen'] }
        }
      }
    })

    expect(result.success).toBe(true)
  })

  it('rejects systems that attach unknown actions or policies', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
        'sales.lead-gen': makeSystem('sales.lead-gen')
      }),
      systems: {
        sales: {
          id: 'sales',
          order: 10,
          label: 'Sales',
          lifecycle: 'active'
        },
        'sales.lead-gen': {
          id: 'sales.lead-gen',
          order: 20,
          label: 'Lead Gen',
          lifecycle: 'active',
          actions: [{ actionId: 'missing.action', intent: 'exposes' }],
          policies: ['missing.policy']
        }
      }
    })

    expect(messages.some((message) => message.includes('references unknown action "missing.action"'))).toBe(true)
    expect(messages.some((message) => message.includes('references unknown policy "missing.policy"'))).toBe(true)
  })
})

describe('knowledge governs validation', () => {
  function makeKnowledgeNode(overrides: Record<string, unknown> = {}) {
    return {
      id: 'knowledge.sales-crm-playbook',
      kind: 'playbook' as const,
      title: 'Sales CRM Playbook',
      summary: 'How CRM work is governed.',
      body: '## CRM',
      updatedAt: '2026-05-10',
      links: [{ target: { kind: 'system', id: 'sales.crm' } }],
      ...overrides
    }
  }

  // Phase 4 (D3): knowledge is now a flat Record<id, OrgKnowledgeNode>.
  // Fixtures updated from { nodes: [...] } to { 'knowledge.id': {...node} }.

  it('accepts typed knowledge links when the target exists', () => {
    const result = OrganizationModelSchema.safeParse({
      ...makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
        'sales.crm': makeSystem('sales.crm', '/crm')
      }),
      knowledge: {
        'knowledge.sales-crm-playbook': makeKnowledgeNode()
      }
    })

    expect(result.success).toBe(true)
  })

  it('rejects typed knowledge links to unknown modeled targets', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
        'sales.crm': makeSystem('sales.crm', '/crm')
      }),
      knowledge: {
        'knowledge.sales-crm-playbook': makeKnowledgeNode({
          links: [{ target: { kind: 'resource', id: 'missing.workflow' } }]
        })
      }
    })

    expect(messages.some((message) => message.includes('references unknown resource target "missing.workflow"'))).toBe(
      true
    )
  })

  it('allows strategy nodes to target systems', () => {
    const result = OrganizationModelSchema.safeParse({
      ...makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
        'sales.crm': makeSystem('sales.crm', '/crm')
      }),
      knowledge: {
        'knowledge.sales-crm-playbook': makeKnowledgeNode({
          kind: 'strategy',
          links: [{ target: { kind: 'system', id: 'sales.crm' } }]
        })
      }
    })

    expect(result.success).toBe(true)
  })

  it('rejects incompatible knowledge kind to target kind pairings', () => {
    const messages = getIssueMessages({
      ...makeMinimalModel({
        sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
        'sales.crm': makeSystem('sales.crm', '/crm')
      }),
      knowledge: {
        'knowledge.sales-crm-playbook': makeKnowledgeNode({
          kind: 'playbook',
          links: [{ target: { kind: 'goal', id: 'missing.goal' } }]
        })
      }
    })

    expect(messages.some((message) => message.includes('kind "playbook" cannot govern goal targets'))).toBe(true)
  })
})
