import { describe, expect, it } from 'vitest'
import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
import { listDomain } from '../helpers'
import { resolveOrganizationModel } from '../resolve'
import type { DeepPartial, OrganizationModel } from '../types'

// ---------------------------------------------------------------------------
// Step 5 of om-flatten-single-array-domains: lock in additive-by-id merge
// across all 9 flattened domains.
//
// Before the flatten, every domain was `{ field: Entry[] }` and deepMerge's
// array branch (`Array.isArray(base) -> return override`) caused tenant
// overrides to wholesale-replace the array. The systems domain needed a
// special-case mergeSystemsById helper (sidebar-regression-fix.mdx OD1) to
// produce additive behavior.
//
// After the flatten, every domain is `Record<string, Entry>` keyed by id, so
// deepMerge's object branch (`isPlainObject(base) && isPlainObject(override)
// -> deepMerge per key`) gives additive-by-id semantics for free.
//
// These tests pin that semantic in place. If anyone re-introduces array shape
// on these domains, or special-cases merge logic, these tests will surface it.
// ---------------------------------------------------------------------------

describe('flatten: deepMerge produces additive-by-id behavior across all 9 flattened domains', () => {
  // Phase 4 (D1): 'statuses' top-level field removed from OrganizationModel.
  // Status data moved into System.ontology.catalogTypes.
  // The remaining 8 domains are all Record<string, Entry> after the flatten.
  const FLATTENED_DOMAINS = [
    'systems',
    'roles',
    'goals',
    'customers',
    'actions',
    'offerings',
    'resources',
    'policies'
  ] as const

  it.each(FLATTENED_DOMAINS)('%s domain shape is Record<string, Entry>, not an array wrapper', (domain) => {
    const value = DEFAULT_ORGANIZATION_MODEL[domain]
    expect(Array.isArray(value)).toBe(false)
    expect(typeof value).toBe('object')
    expect(value).not.toBeNull()
  })

  it('adds a tenant system to the merged record without dropping defaults', () => {
    const baseSize = Object.keys(DEFAULT_ORGANIZATION_MODEL.systems).length

    const model = resolveOrganizationModel({
      systems: {
        'tenant.workspace': {
          id: 'tenant.workspace',
          order: 10000,
          label: 'Tenant Workspace',
          lifecycle: 'active',
          path: '/tenant'
        }
      }
    })

    expect(Object.keys(model.systems)).toHaveLength(baseSize + 1)
    expect(model.systems['tenant.workspace']?.label).toBe('Tenant Workspace')
  })

  it('merges a tenant role additively without clobbering default roles', () => {
    const baseSize = Object.keys(DEFAULT_ORGANIZATION_MODEL.roles).length

    const model = resolveOrganizationModel({
      roles: {
        'role.tenant-lead': {
          id: 'role.tenant-lead',
          order: 10000,
          title: 'Tenant Lead'
        }
      }
    })

    expect(Object.keys(model.roles)).toHaveLength(baseSize + 1)
    expect(model.roles['role.tenant-lead']?.title).toBe('Tenant Lead')
  })

  it('merges a tenant action additively while preserving the default action catalog', () => {
    const baseSize = Object.keys(DEFAULT_ORGANIZATION_MODEL.actions).length

    const model = resolveOrganizationModel({
      actions: {
        'tenant.custom-action': {
          id: 'tenant.custom-action',
          order: 10000,
          label: 'Custom Action',
          scope: 'global',
          resourceId: 'tenant-workflow'
        }
      }
    })

    expect(Object.keys(model.actions)).toHaveLength(baseSize + 1)
    expect(model.actions['tenant.custom-action']?.label).toBe('Custom Action')
  })

  it('overrides an existing system by id (last write wins on the same map key)', () => {
    const model = resolveOrganizationModel({
      systems: {
        dashboard: {
          id: 'dashboard',
          order: 10,
          label: 'Tenant Dashboard',
          lifecycle: 'active',
          path: '/'
        }
      }
    })

    expect(model.systems['dashboard']?.label).toBe('Tenant Dashboard')
  })

  it('adds a complete system entry when no generic default entry exists', () => {
    const model = resolveOrganizationModel({
      systems: {
        dashboard: {
          id: 'dashboard',
          order: 10,
          label: 'Renamed',
          path: '/'
        }
      }
    })

    const merged = model.systems['dashboard']
    expect(merged?.label).toBe('Renamed')
    expect(merged?.path).toBe('/')
  })

  it('confirms no special-case mergeSystemsById helper exists in resolve.ts', async () => {
    // The OD1 special case from sidebar-regression-fix.mdx is superseded by
    // this flatten. If anyone reintroduces it under any of these names,
    // surface that drift.
    const source = await import('node:fs/promises').then((fs) =>
      fs.readFile(new URL('../resolve.ts', import.meta.url), 'utf-8')
    )

    expect(source).not.toMatch(/mergeSystemsById/)
    expect(source).not.toMatch(/mergeById/)
    expect(source).not.toMatch(/mergeRecordById/)
  })

  it('multiple flattened domains receive independent additive merges in one call', () => {
    const baseSystems = Object.keys(DEFAULT_ORGANIZATION_MODEL.systems).length
    const baseRoles = Object.keys(DEFAULT_ORGANIZATION_MODEL.roles).length
    const baseActions = Object.keys(DEFAULT_ORGANIZATION_MODEL.actions).length

    const model = resolveOrganizationModel({
      systems: {
        'tenant.a': { id: 'tenant.a', order: 10000, label: 'A', lifecycle: 'active', path: '/a' }
      },
      roles: {
        'role.a': { id: 'role.a', order: 10000, title: 'A' }
      },
      actions: {
        'tenant.a': {
          id: 'tenant.a',
          order: 10000,
          label: 'A Action',
          scope: 'global',
          resourceId: 'a-workflow'
        }
      }
    })

    expect(Object.keys(model.systems)).toHaveLength(baseSystems + 1)
    expect(Object.keys(model.roles)).toHaveLength(baseRoles + 1)
    expect(Object.keys(model.actions)).toHaveLength(baseActions + 1)
  })

  // ---------------------------------------------------------------------------
  // Override merge edge cases
  // ---------------------------------------------------------------------------

  it('order override on existing entry takes precedence and re-sorts the listed domain', () => {
    const override: DeepPartial<OrganizationModel> = {
      systems: {
        dashboard: {
          id: 'dashboard',
          order: 1000,
          label: 'Dashboard',
          lifecycle: 'active',
          path: '/'
        }
      }
    }

    const resolved = resolveOrganizationModel(override)

    expect(resolved.systems['dashboard']!.order).toBe(1000)

    const listed = listDomain(resolved.systems)
    const dashboardIndex = listed.findIndex((s) => s.id === 'dashboard')
    const systemsBefore = listed.filter((s) => s.order < 1000)
    expect(dashboardIndex).toBeGreaterThan(systemsBefore.length - 1)
    // All entries with order < 1000 must appear before dashboard in the sorted list
    systemsBefore.forEach((s) => {
      expect(listed.indexOf(s)).toBeLessThan(dashboardIndex)
    })
  })

  it('override entry omitting required order is rejected when no base entry exists', () => {
    const override: DeepPartial<OrganizationModel> = {
      systems: {
        dashboard: {
          id: 'dashboard',
          label: 'Renamed Dashboard'
        }
      }
    }

    expect(() => resolveOrganizationModel(override)).toThrow()
  })

  it('throws when system map key does not match the entry id', () => {
    const badOverride: DeepPartial<OrganizationModel> = {
      systems: {
        'wrong-key': {
          id: 'different-id',
          order: 10,
          label: 'Mismatch',
          lifecycle: 'active',
          path: '/mismatch'
        }
      }
    }

    expect(() => resolveOrganizationModel(badOverride)).toThrow(/map key/)
  })

  it('replaces nested arrays rather than merging them', () => {
    // sales.lead-gen has a top-level `actions` array field on the entry.
    // deepMerge hits Array.isArray(existing) => returns override value directly.
    const singleRef = [{ actionId: 'lead-gen.company.source', intent: 'exposes' as const }]

    const override: DeepPartial<OrganizationModel> = {
      actions: {
        'lead-gen.company.source': {
          id: 'lead-gen.company.source',
          order: 10,
          label: 'Source companies'
        }
      },
      systems: {
        'sales.lead-gen': {
          id: 'sales.lead-gen',
          order: 10,
          label: 'Lead Gen',
          actions: singleRef
        }
      }
    }

    const resolved = resolveOrganizationModel(override)
    const entry = resolved.systems['sales.lead-gen']!

    expect(entry.actions).toHaveLength(1)
    expect(entry.actions).toEqual(singleRef)
  })

  it('empty systems override produces same systems domain as no override', () => {
    const withEmpty = resolveOrganizationModel({ systems: {} })
    const withNone = resolveOrganizationModel()

    expect(withEmpty.systems).toEqual(withNone.systems)
  })

  it('field set to undefined in override is skipped and retains the base value', () => {
    const override: DeepPartial<OrganizationModel> = {
      systems: {
        dashboard: {
          id: 'dashboard',
          order: 10,
          label: 'Dashboard',
          path: undefined
        }
      }
    }

    const resolved = resolveOrganizationModel(override)

    expect(resolved.systems['dashboard']!.path).toBeUndefined()
  })

  it('simultaneous override of systems, roles, and actions merges each domain independently without cross-contamination', () => {
    const baseSystems = Object.keys(DEFAULT_ORGANIZATION_MODEL.systems).length
    const baseRoles = Object.keys(DEFAULT_ORGANIZATION_MODEL.roles).length
    const baseActions = Object.keys(DEFAULT_ORGANIZATION_MODEL.actions).length
    const baseCustomers = DEFAULT_ORGANIZATION_MODEL.customers
    const baseGoals = DEFAULT_ORGANIZATION_MODEL.goals

    const model = resolveOrganizationModel({
      systems: {
        'multi.sys': { id: 'multi.sys', order: 50000, label: 'Multi Sys', lifecycle: 'active', path: '/multi-sys' }
      },
      roles: {
        'multi.role': { id: 'multi.role', order: 50000, title: 'Multi Role' }
      },
      actions: {
        'multi.action': {
          id: 'multi.action',
          order: 50000,
          label: 'Multi Action',
          scope: 'global',
          resourceId: 'multi-workflow'
        }
      }
    })

    expect(Object.keys(model.systems)).toHaveLength(baseSystems + 1)
    expect(model.systems['multi.sys']).toBeDefined()

    expect(Object.keys(model.roles)).toHaveLength(baseRoles + 1)
    expect(model.roles['multi.role']).toBeDefined()

    expect(Object.keys(model.actions)).toHaveLength(baseActions + 1)
    expect(model.actions['multi.action']).toBeDefined()

    expect(model.customers).toEqual(baseCustomers)
    expect(model.goals).toEqual(baseGoals)
  })

  it('override sets top-level array field on entry to single-element array replacing the original', () => {
    // The `invocations` field on an action entry is a top-level array.
    // deepMerge sees Array.isArray(existing) and returns override directly.
    const replacementInvocations = [{ kind: 'slash-command' as const, command: '/custom-source' }]

    const override: DeepPartial<OrganizationModel> = {
      actions: {
        'lead-gen.company.source': {
          id: 'lead-gen.company.source',
          order: 10,
          label: 'Source companies',
          invocations: replacementInvocations
        }
      }
    }

    const resolved = resolveOrganizationModel(override)
    const entry = resolved.actions['lead-gen.company.source']!

    expect(entry.invocations).toHaveLength(1)
    expect(entry.invocations).toEqual(replacementInvocations)
  })
})
