import { describe, expect, it } from 'vitest'
import type { AcqDealRow } from './types'
import type { Action, ActionDef } from './derive-actions'
import { SendReplyActionPayloadSchema, deriveActions as deriveActionsCore } from './derive-actions'

// ---------------------------------------------------------------------------
// Fixture builder
// ---------------------------------------------------------------------------

/**
 * Build a minimal AcqDealRow suitable for `isAvailableFor()` predicate checks.
 * Only stage_key and state_key affect availability logic; all other required
 * columns are filled with safe, inert defaults.
 */
function makeDeal(
  overrides: Partial<AcqDealRow> & { ownership?: 'us' | 'them' | null; nextAction?: string | null } = {}
): AcqDealRow & { ownership?: 'us' | 'them' | null; nextAction?: string | null } {
  return {
    id: 'deal-fixture-id',
    organization_id: 'org-fixture-id',
    contact_id: null,
    contact_email: 'fixture@example.com',
    pipeline_key: 'crm',
    stage_key: null,
    state_key: null,
    activity_log: [],
    closed_lost_at: null,
    closed_lost_reason: null,
    discovery_data: null,
    discovery_submitted_at: null,
    discovery_submitted_by: null,
    initial_fee: null,
    monthly_fee: null,
    payment_link_sent_at: null,
    payment_received_at: null,
    proposal_data: null,
    proposal_generated_at: null,
    proposal_pdf_url: null,
    proposal_reviewed_at: null,
    proposal_reviewed_by: null,
    proposal_sent_at: null,
    proposal_signed_at: null,
    signature_envelope_id: null,
    source_list_id: null,
    source_type: null,
    stripe_payment_id: null,
    stripe_payment_link: null,
    stripe_payment_link_id: null,
    stripe_subscription_id: null,
    created_at: '2026-01-01T00:00:00.000Z',
    updated_at: '2026-01-01T00:00:00.000Z',
    ...overrides
  }
}

const DEFAULT_CRM_ACTIONS: ActionDef[] = [
  {
    key: 'move_to_proposal',
    label: 'Move to Proposal',
    workflowId: 'move_to_proposal-workflow',
    isAvailableFor: (deal) => deal.stage_key === 'interested'
  },
  {
    key: 'move_to_closing',
    label: 'Move to Closing',
    workflowId: 'move_to_closing-workflow',
    isAvailableFor: (deal) => deal.stage_key === 'proposal'
  },
  {
    key: 'move_to_closed_won',
    label: 'Close Won',
    workflowId: 'move_to_closed_won-workflow',
    isAvailableFor: (deal) => deal.stage_key === 'closing'
  },
  {
    key: 'move_to_closed_lost',
    label: 'Close Lost',
    workflowId: 'move_to_closed_lost-workflow',
    isAvailableFor: (deal) =>
      deal.stage_key === 'interested' || deal.stage_key === 'proposal' || deal.stage_key === 'closing'
  },
  {
    key: 'move_to_nurturing',
    label: 'Move to Nurturing',
    workflowId: 'move_to_nurturing-workflow',
    isAvailableFor: (deal) =>
      deal.stage_key === 'interested' || deal.stage_key === 'proposal' || deal.stage_key === 'closing'
  },
  {
    key: 'send_reply',
    label: 'Send Reply',
    workflowId: 'crm-send-reply-workflow',
    payloadSchema: SendReplyActionPayloadSchema,
    isAvailableFor: (deal) =>
      deal.stage_key === 'interested' &&
      deal.nextAction === 'send_reply' &&
      (deal.state_key === 'discovery_replied' ||
        deal.state_key === 'discovery_link_sent' ||
        deal.state_key === 'discovery_nudging')
  },
  {
    key: 'send_link',
    label: 'Send Booking Link',
    workflowId: 'crm-send-booking-link-workflow',
    isAvailableFor: (deal) => deal.stage_key === 'interested' && deal.state_key === 'discovery_replied'
  },
  {
    key: 'send_nudge',
    label: 'Send Nudge',
    workflowId: 'crm-send-nudge-workflow',
    isAvailableFor: (deal) =>
      deal.stage_key === 'interested' &&
      (deal.state_key === 'discovery_link_sent' || deal.state_key === 'discovery_nudging')
  },
  {
    key: 'mark_no_show',
    label: 'Mark No-Show',
    workflowId: 'mark_no_show-workflow',
    isAvailableFor: (deal) => deal.stage_key === 'interested' && deal.state_key === 'discovery_nudging'
  },
  {
    key: 'rebook',
    label: 'Rebook',
    workflowId: 'crm-rebook-workflow',
    isAvailableFor: (deal) =>
      deal.stage_key === 'interested' && deal.state_key === 'discovery_booking_cancelled'
  }
]

function deriveActions(deal: Parameters<typeof deriveActionsCore>[0], actions: ActionDef[] = DEFAULT_CRM_ACTIONS): Action[] {
  return deriveActionsCore(deal, actions)
}

// ---------------------------------------------------------------------------
// DEFAULT_CRM_ACTIONS static contract
// ---------------------------------------------------------------------------

describe('DEFAULT_CRM_ACTIONS static contract', () => {
  const EXPECTED_KEYS = [
    'move_to_proposal',
    'move_to_closing',
    'move_to_closed_won',
    'move_to_closed_lost',
    'move_to_nurturing',
    'send_reply',
    'send_link',
    'send_nudge',
    'mark_no_show',
    'rebook'
  ]

  it('contains all 10 expected action keys', () => {
    const keys = DEFAULT_CRM_ACTIONS.map((a) => a.key)
    expect(keys).toEqual(EXPECTED_KEYS)
  })

  it('every entry has key, label, isAvailableFor, and workflowId', () => {
    for (const def of DEFAULT_CRM_ACTIONS) {
      expect(typeof def.key).toBe('string')
      expect(def.key.length).toBeGreaterThan(0)
      expect(typeof def.label).toBe('string')
      expect(def.label.length).toBeGreaterThan(0)
      expect(typeof def.isAvailableFor).toBe('function')
      expect(typeof def.workflowId).toBe('string')
      expect(def.workflowId.length).toBeGreaterThan(0)
    }
  })

  it('no two ActionDefs share the same key', () => {
    const keys = DEFAULT_CRM_ACTIONS.map((a) => a.key)
    const unique = new Set(keys)
    expect(unique.size).toBe(keys.length)
  })

  it('derives promoted CRM runtime metadata from the local runtime contract', () => {
    const sendReply = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'send_reply')

    expect(sendReply).toBeDefined()
    expect(sendReply?.label).toBe('Send Reply')
    expect(sendReply?.workflowId).toBe('crm-send-reply-workflow')
  })

  it('keeps mark_no_show as a runtime-only CRM action', () => {
    const markNoShow = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'mark_no_show')

    expect(markNoShow).toBeDefined()
    expect(markNoShow?.workflowId).toBe('mark_no_show-workflow')
  })
})

// ---------------------------------------------------------------------------
// isAvailableFor() predicates — per stage / state
// ---------------------------------------------------------------------------

describe('isAvailableFor() predicates', () => {
  describe('move_to_proposal', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'move_to_proposal')!

    it('is available for interested stage', () => {
      expect(def.isAvailableFor(makeDeal({ stage_key: 'interested' }))).toBe(true)
    })

    it.each(['proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing', null])(
      'is NOT available for stage %s',
      (stage) => {
        expect(def.isAvailableFor(makeDeal({ stage_key: stage }))).toBe(false)
      }
    )
  })

  describe('move_to_closing', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'move_to_closing')!

    it('is available for proposal stage', () => {
      expect(def.isAvailableFor(makeDeal({ stage_key: 'proposal' }))).toBe(true)
    })

    it.each(['interested', 'closing', 'closed_won', 'closed_lost', 'nurturing', null])(
      'is NOT available for stage %s',
      (stage) => {
        expect(def.isAvailableFor(makeDeal({ stage_key: stage }))).toBe(false)
      }
    )
  })

  describe('move_to_closed_won', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'move_to_closed_won')!

    it('is available for closing stage', () => {
      expect(def.isAvailableFor(makeDeal({ stage_key: 'closing' }))).toBe(true)
    })

    it.each(['interested', 'proposal', 'closed_won', 'closed_lost', 'nurturing', null])(
      'is NOT available for stage %s',
      (stage) => {
        expect(def.isAvailableFor(makeDeal({ stage_key: stage }))).toBe(false)
      }
    )
  })

  describe('move_to_closed_lost', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'move_to_closed_lost')!

    it.each(['interested', 'proposal', 'closing'])('is available for stage %s', (stage) => {
      expect(def.isAvailableFor(makeDeal({ stage_key: stage }))).toBe(true)
    })

    it.each(['closed_won', 'closed_lost', 'nurturing', null])('is NOT available for stage %s', (stage) => {
      expect(def.isAvailableFor(makeDeal({ stage_key: stage }))).toBe(false)
    })
  })

  describe('move_to_nurturing', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'move_to_nurturing')!

    it.each(['interested', 'proposal', 'closing'])('is available for stage %s', (stage) => {
      expect(def.isAvailableFor(makeDeal({ stage_key: stage }))).toBe(true)
    })

    it.each(['closed_won', 'closed_lost', 'nurturing', null])('is NOT available for stage %s', (stage) => {
      expect(def.isAvailableFor(makeDeal({ stage_key: stage }))).toBe(false)
    })
  })

  describe('send_link', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'send_link')!

    it('is available for interested/discovery_replied', () => {
      expect(def.isAvailableFor(makeDeal({ stage_key: 'interested', state_key: 'discovery_replied' }))).toBe(true)
    })

    it.each([
      { stage_key: 'interested', state_key: null },
      { stage_key: 'interested', state_key: 'discovery_link_sent' },
      { stage_key: 'interested', state_key: 'discovery_nudging' },
      { stage_key: 'interested', state_key: 'discovery_booking_cancelled' },
      { stage_key: 'proposal', state_key: 'discovery_replied' },
      { stage_key: null, state_key: null }
    ])('is NOT available for %o', (overrides) => {
      expect(def.isAvailableFor(makeDeal(overrides))).toBe(false)
    })
  })

  describe('send_nudge', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'send_nudge')!

    it.each(['discovery_link_sent', 'discovery_nudging'])('is available for interested/%s', (state) => {
      expect(def.isAvailableFor(makeDeal({ stage_key: 'interested', state_key: state }))).toBe(true)
    })

    it.each([
      { stage_key: 'interested', state_key: null },
      { stage_key: 'interested', state_key: 'discovery_replied' },
      { stage_key: 'interested', state_key: 'discovery_booking_cancelled' },
      { stage_key: 'proposal', state_key: 'discovery_link_sent' },
      { stage_key: null, state_key: null }
    ])('is NOT available for %o', (overrides) => {
      expect(def.isAvailableFor(makeDeal(overrides))).toBe(false)
    })
  })

  describe('mark_no_show', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'mark_no_show')!

    it('is available for interested/discovery_nudging', () => {
      expect(def.isAvailableFor(makeDeal({ stage_key: 'interested', state_key: 'discovery_nudging' }))).toBe(true)
    })

    it.each([
      { stage_key: 'interested', state_key: null },
      { stage_key: 'interested', state_key: 'discovery_replied' },
      { stage_key: 'interested', state_key: 'discovery_link_sent' },
      { stage_key: 'interested', state_key: 'discovery_booking_cancelled' },
      { stage_key: 'proposal', state_key: 'discovery_nudging' },
      { stage_key: null, state_key: null }
    ])('is NOT available for %o', (overrides) => {
      expect(def.isAvailableFor(makeDeal(overrides))).toBe(false)
    })
  })

  describe('rebook', () => {
    const def = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'rebook')!

    it('is available for interested/discovery_booking_cancelled', () => {
      expect(def.isAvailableFor(makeDeal({ stage_key: 'interested', state_key: 'discovery_booking_cancelled' }))).toBe(
        true
      )
    })

    it.each([
      { stage_key: 'interested', state_key: null },
      { stage_key: 'interested', state_key: 'discovery_replied' },
      { stage_key: 'interested', state_key: 'discovery_link_sent' },
      { stage_key: 'interested', state_key: 'discovery_nudging' },
      { stage_key: 'proposal', state_key: 'discovery_booking_cancelled' },
      { stage_key: null, state_key: null }
    ])('is NOT available for %o', (overrides) => {
      expect(def.isAvailableFor(makeDeal(overrides))).toBe(false)
    })
  })

  describe('terminal stages produce empty action lists', () => {
    it.each(['closed_won', 'closed_lost', 'nurturing', null])('returns [] for stage %s', (stage) => {
      expect(deriveActions(makeDeal({ stage_key: stage }))).toEqual([])
    })
  })
})

// ---------------------------------------------------------------------------
// deriveActions() integration
// ---------------------------------------------------------------------------

describe('deriveActions()', () => {
  it('returns render-time shape { key, label } only — no isAvailableFor, workflowId, or payload fields', () => {
    const actions = deriveActions(makeDeal({ stage_key: 'interested' }))

    expect(actions.length).toBeGreaterThan(0)
    for (const action of actions) {
      expect(action).not.toHaveProperty('isAvailableFor')
      expect(action).not.toHaveProperty('workflowId')
      // Old fields from prior shape
      expect(action).not.toHaveProperty('available')
      expect(action).not.toHaveProperty('handler')
      expect(action).not.toHaveProperty('kind')
      expect(action).not.toHaveProperty('payload')
      // Required render-time fields
      expect(action).toHaveProperty('key')
      expect(action).toHaveProperty('label')
    }
  })

  it('actions without payloadSchema render with payloadSchema undefined (button branch)', () => {
    const actions = deriveActions(makeDeal({ stage_key: 'interested' }))
    for (const action of actions) {
      expect(action.payloadSchema).toBeUndefined()
    }
  })

  it('custom action set works: deriveActions(deal, [customAction]) filters by the override', () => {
    const alwaysAvailableAction: ActionDef = {
      key: 'custom_action',
      label: 'Custom Action',
      isAvailableFor: () => true,
      workflowId: 'custom-action-workflow'
    }
    const neverAvailableAction: ActionDef = {
      key: 'blocked_action',
      label: 'Blocked Action',
      isAvailableFor: () => false,
      workflowId: 'blocked-action-workflow'
    }

    const actions = deriveActions(makeDeal({ stage_key: 'interested' }), [alwaysAvailableAction, neverAvailableAction])

    expect(actions).toEqual([{ key: 'custom_action', label: 'Custom Action' }])
  })

  it('empty action set returns empty array', () => {
    expect(deriveActions(makeDeal({ stage_key: 'interested' }), [])).toEqual([])
  })

  it('core deriveActions returns [] when no action catalog is provided', () => {
    expect(deriveActionsCore(makeDeal({ stage_key: 'proposal' }))).toEqual([])
  })
})

// ---------------------------------------------------------------------------
// SendReplyActionPayloadSchema boundary tests
// ---------------------------------------------------------------------------

describe('SendReplyActionPayloadSchema', () => {
  it('rejects empty string', () => {
    expect(SendReplyActionPayloadSchema.safeParse({ replyBody: '' }).success).toBe(false)
  })

  it('rejects whitespace-only string after trim', () => {
    expect(SendReplyActionPayloadSchema.safeParse({ replyBody: '   ' }).success).toBe(false)
  })

  it('accepts single character after trim', () => {
    expect(SendReplyActionPayloadSchema.safeParse({ replyBody: 'x' }).success).toBe(true)
  })

  it('accepts string at the 10000-character limit', () => {
    expect(SendReplyActionPayloadSchema.safeParse({ replyBody: 'x'.repeat(10000) }).success).toBe(true)
  })

  it('rejects string exceeding 10000 characters', () => {
    expect(SendReplyActionPayloadSchema.safeParse({ replyBody: 'x'.repeat(10001) }).success).toBe(false)
  })

  it('trims leading and trailing whitespace from replyBody', () => {
    const result = SendReplyActionPayloadSchema.safeParse({ replyBody: '  hello  ' })
    expect(result.success).toBe(true)
    if (result.success) {
      expect(result.data.replyBody).toBe('hello')
    }
  })

  it('rejects unknown fields (strict schema)', () => {
    expect(SendReplyActionPayloadSchema.safeParse({ replyBody: 'hello', extra: 'field' }).success).toBe(false)
  })
})

// ---------------------------------------------------------------------------
// deriveActions() — custom action set isolation
// ---------------------------------------------------------------------------

describe('deriveActions() — custom action set isolation', () => {
  it('filters only against the provided set — does not merge with DEFAULT_CRM_ACTIONS', () => {
    const customOnly: ActionDef = {
      key: 'custom_only',
      label: 'Custom Only',
      isAvailableFor: () => true,
      workflowId: 'custom-only-workflow'
    }

    const result = deriveActions(makeDeal({ stage_key: 'interested' }), [customOnly])

    const resultKeys = result.map((a) => a.key)
    expect(resultKeys).toEqual(['custom_only'])
    const defaultKeys = DEFAULT_CRM_ACTIONS.map((a) => a.key)
    for (const key of defaultKeys) {
      expect(resultKeys).not.toContain(key)
    }
  })

  it('always-available custom action appears for any deal state', () => {
    const alwaysOn: ActionDef = {
      key: 'always_on',
      label: 'Always On',
      isAvailableFor: () => true,
      workflowId: 'always-on-workflow'
    }

    for (const stage of ['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing', null]) {
      const result = deriveActions(makeDeal({ stage_key: stage }), [alwaysOn])
      expect(result).toEqual([{ key: 'always_on', label: 'Always On' }])
    }
  })
})

// ---------------------------------------------------------------------------
// deriveActions() — action metadata preservation
// ---------------------------------------------------------------------------

describe('deriveActions() — action metadata preservation', () => {
  it('send_reply action includes payloadSchema in output', () => {
    const actions = deriveActions(
      makeDeal({ stage_key: 'interested', state_key: 'discovery_replied', nextAction: 'send_reply' })
    )
    const sendReply = actions.find((a) => a.key === 'send_reply')
    expect(sendReply).toBeDefined()
    expect(sendReply?.payloadSchema).toBeDefined()
    expect(sendReply?.payloadSchema).toBe(SendReplyActionPayloadSchema)
  })

  it('move_to_proposal does NOT include payloadSchema', () => {
    const actions = deriveActions(makeDeal({ stage_key: 'interested' }))
    const action = actions.find((a) => a.key === 'move_to_proposal')
    expect(action).toBeDefined()
    expect(action?.payloadSchema).toBeUndefined()
  })

  it('returned actions preserve key and label properties', () => {
    const actions = deriveActions(makeDeal({ stage_key: 'interested' }))
    expect(actions.length).toBeGreaterThan(0)
    for (const action of actions) {
      expect(typeof action.key).toBe('string')
      expect(action.key.length).toBeGreaterThan(0)
      expect(typeof action.label).toBe('string')
      expect(action.label.length).toBeGreaterThan(0)
    }
  })
})

// ---------------------------------------------------------------------------
// deriveActions() — deduplication and ordering
// ---------------------------------------------------------------------------

describe('deriveActions() — deduplication and ordering', () => {
  it.each([
    { stage_key: 'interested', state_key: null },
    { stage_key: 'interested', state_key: 'discovery_replied' },
    { stage_key: 'interested', state_key: 'discovery_link_sent' },
    { stage_key: 'interested', state_key: 'discovery_nudging' },
    { stage_key: 'interested', state_key: 'discovery_booking_cancelled' },
    { stage_key: 'proposal', state_key: null },
    { stage_key: 'closing', state_key: null }
  ] as const)('no duplicate action keys for %o', (overrides) => {
    const actions = deriveActions(makeDeal(overrides))
    const keys = actions.map((a) => a.key)
    expect(new Set(keys).size).toBe(keys.length)
  })

  it('returns the same order for repeated calls with identical input', () => {
    const deal = makeDeal({ stage_key: 'interested', state_key: 'discovery_replied' })
    const first = deriveActions(deal).map((a) => a.key)
    const second = deriveActions(deal).map((a) => a.key)
    expect(first).toEqual(second)
  })
})

// ---------------------------------------------------------------------------
// deriveActions() — multi-state discovery path coverage
// ---------------------------------------------------------------------------

describe('deriveActions() — multi-state discovery path coverage', () => {
  it('interested + null state: base actions only, no state-specific actions', () => {
    expect(deriveActions(makeDeal({ stage_key: 'interested', state_key: null }))).toEqual([
      { key: 'move_to_proposal', label: 'Move to Proposal' },
      { key: 'move_to_closed_lost', label: 'Close Lost' },
      { key: 'move_to_nurturing', label: 'Move to Nurturing' }
    ])
  })

  it('interested + discovery_replied: base actions plus send_link when no next move signal is available', () => {
    expect(deriveActions(makeDeal({ stage_key: 'interested', state_key: 'discovery_replied' }))).toEqual([
      { key: 'move_to_proposal', label: 'Move to Proposal' },
      { key: 'move_to_closed_lost', label: 'Close Lost' },
      { key: 'move_to_nurturing', label: 'Move to Nurturing' },
      { key: 'send_link', label: 'Send Booking Link' }
    ])
  })

  it('interested + discovery_link_sent: base actions plus send_nudge', () => {
    expect(deriveActions(makeDeal({ stage_key: 'interested', state_key: 'discovery_link_sent' }))).toEqual([
      { key: 'move_to_proposal', label: 'Move to Proposal' },
      { key: 'move_to_closed_lost', label: 'Close Lost' },
      { key: 'move_to_nurturing', label: 'Move to Nurturing' },
      { key: 'send_nudge', label: 'Send Nudge' }
    ])
  })

  it('interested + discovery_nudging: base actions plus send_nudge and mark_no_show', () => {
    expect(deriveActions(makeDeal({ stage_key: 'interested', state_key: 'discovery_nudging' }))).toEqual([
      { key: 'move_to_proposal', label: 'Move to Proposal' },
      { key: 'move_to_closed_lost', label: 'Close Lost' },
      { key: 'move_to_nurturing', label: 'Move to Nurturing' },
      { key: 'send_nudge', label: 'Send Nudge' },
      { key: 'mark_no_show', label: 'Mark No-Show' }
    ])
  })
})

// ---------------------------------------------------------------------------
// deriveActions() - ownership-gated reply action
// ---------------------------------------------------------------------------

describe('deriveActions() - ownership-gated reply action', () => {
  it('lead asks us to respond: send_reply is available and ownership is our move', () => {
    const actions = deriveActions(
      makeDeal({
        stage_key: 'interested',
        state_key: 'discovery_replied',
        nextAction: 'send_reply',
        activity_log: [{ type: 'reply_received', timestamp: '2026-01-10T10:00:00Z' }]
      })
    )

    expect(actions).toContainEqual({ key: 'send_reply', label: 'Send Reply', payloadSchema: SendReplyActionPayloadSchema })
  })

  it('lead deferred next step: send_reply is not available while it is their move', () => {
    const actions = deriveActions(
      makeDeal({
        stage_key: 'interested',
        state_key: 'discovery_replied',
        activity_log: [
          { type: 'reply_received', timestamp: '2026-01-10T10:00:00Z' },
          { type: 'lead_deferred_next_step', timestamp: '2026-01-10T10:05:00Z' }
        ]
      })
    )

    expect(actions.map((action) => action.key)).not.toContain('send_reply')
    expect(actions).toContainEqual({ key: 'send_link', label: 'Send Booking Link' })
  })

  it('outbound after lead response: send_reply is not available while it is their move', () => {
    const actions = deriveActions(
      makeDeal({
        stage_key: 'interested',
        state_key: 'discovery_replied',
        activity_log: [
          { type: 'reply_received', timestamp: '2026-01-10T10:00:00Z' },
          { type: 'reply_sent_to_lead', timestamp: '2026-01-10T10:05:00Z' }
        ]
      })
    )

    expect(actions.map((action) => action.key)).not.toContain('send_reply')
    expect(actions).toContainEqual({ key: 'send_link', label: 'Send Booking Link' })
  })

  it('uses projected nextAction before deriving from activity_log', () => {
    const actions = deriveActions(
      makeDeal({
        stage_key: 'interested',
        state_key: 'discovery_replied',
        nextAction: null,
        activity_log: [{ type: 'reply_received', timestamp: '2026-01-10T10:00:00Z' }]
      })
    )

    expect(actions.map((action) => action.key)).not.toContain('send_reply')
  })
})

// ---------------------------------------------------------------------------
// Type-level smoke tests (compile-time only, no runtime assertions)
// ---------------------------------------------------------------------------

describe('type-level smoke', () => {
  it('Action and ActionDef are usable as TypeScript types', () => {
    // If this file compiles without error, these types are valid at the use-site.
    function acceptsAction(_action: Action): void {}
    function acceptsActionDef(_def: ActionDef): void {}

    const sampleAction: Action = { key: 'test', label: 'Test' }
    const sampleDef: ActionDef = {
      key: 'test',
      label: 'Test',
      isAvailableFor: () => true,
      workflowId: 'test-workflow'
    }

    acceptsAction(sampleAction)
    acceptsActionDef(sampleDef)

    // No runtime assertion needed — compilation success is the contract
    expect(true).toBe(true)
  })
})
