/**
 * Unit tests for the assignment policy resolution engine.
 *
 * These are pure-logic tests — no database or Prisma needed.
 * We construct MemberContext objects inline and verify the resolved policies.
 */

import { describe, it, expect } from 'vitest'
import type { CapMode } from '@prisma/client'
import type { MemberContext } from '@/server/services/competition-context'
import {
  SYSTEM_DEFAULT_CAP,
  SYSTEM_DEFAULT_CAP_MODE,
  SYSTEM_DEFAULT_SOFT_BUFFER,
  resolveEffectiveCap,
  resolveEffectiveCapMode,
  resolveEffectiveSoftCapBuffer,
  resolveEffectiveCategoryBias,
  evaluateAssignmentPolicy,
} from '@/server/services/assignment-policy'

// ============================================================================
// Helpers — build minimal MemberContext stubs
// ============================================================================

function baseMemberContext(overrides: Partial<MemberContext> = {}): MemberContext {
  return {
    competition: {} as any,
    round: {} as any,
    roundConfig: {} as any,
    submissionWindows: [],
    juryGroup: null,
    member: {
      id: 'member-1',
      juryGroupId: 'jg-1',
      userId: 'user-1',
      role: 'MEMBER',
      maxAssignmentsOverride: null,
      capModeOverride: null,
      categoryQuotasOverride: null,
      preferredStartupRatio: null,
      availabilityNotes: null,
      selfServiceCap: null,
      selfServiceRatio: null,
      joinedAt: new Date(),
    } as any,
    user: { id: 'user-1', name: 'Test Juror', email: 'test@example.com', role: 'JURY_MEMBER' },
    currentAssignmentCount: 0,
    assignmentsByCategory: {},
    pendingIntents: [],
    ...overrides,
  }
}

function withJuryGroup(
  ctx: MemberContext,
  groupOverrides: Record<string, unknown> = {},
): MemberContext {
  const defaultGroup = {
    id: 'jg-1',
    competitionId: 'comp-1',
    name: 'Panel A',
    slug: 'panel-a',
    description: null,
    sortOrder: 0,
    defaultMaxAssignments: 20,
    defaultCapMode: 'SOFT' as CapMode,
    softCapBuffer: 3,
    categoryQuotasEnabled: false,
    defaultCategoryQuotas: null,
    allowJurorCapAdjustment: false,
    allowJurorRatioAdjustment: false,
    createdAt: new Date(),
    updatedAt: new Date(),
    ...groupOverrides,
  }
  return { ...ctx, juryGroup: defaultGroup as any }
}

// ============================================================================
// resolveEffectiveCap
// ============================================================================

describe('resolveEffectiveCap', () => {
  it('returns system default (15) when no jury group', () => {
    const ctx = baseMemberContext()
    const result = resolveEffectiveCap(ctx)
    expect(result.value).toBe(SYSTEM_DEFAULT_CAP)
    expect(result.source).toBe('system')
  })

  it('returns jury group default when group is set', () => {
    const ctx = withJuryGroup(baseMemberContext(), { defaultMaxAssignments: 25 })
    const result = resolveEffectiveCap(ctx)
    expect(result.value).toBe(25)
    expect(result.source).toBe('jury_group')
  })

  it('admin per-member override takes precedence over group default', () => {
    let ctx = withJuryGroup(baseMemberContext(), { defaultMaxAssignments: 25 })
    ctx = { ...ctx, member: { ...ctx.member, maxAssignmentsOverride: 10 } }
    const result = resolveEffectiveCap(ctx)
    expect(result.value).toBe(10)
    expect(result.source).toBe('member')
    expect(result.explanation).toContain('Admin per-member override')
  })

  it('self-service cap overrides admin when allowJurorCapAdjustment is true', () => {
    let ctx = withJuryGroup(baseMemberContext(), {
      defaultMaxAssignments: 25,
      allowJurorCapAdjustment: true,
    })
    ctx = { ...ctx, member: { ...ctx.member, selfServiceCap: 12 } }
    const result = resolveEffectiveCap(ctx)
    expect(result.value).toBe(12)
    expect(result.source).toBe('member')
    expect(result.explanation).toContain('Self-service cap')
  })

  it('self-service cap is bounded by admin max (override)', () => {
    let ctx = withJuryGroup(baseMemberContext(), {
      defaultMaxAssignments: 25,
      allowJurorCapAdjustment: true,
    })
    ctx = {
      ...ctx,
      member: {
        ...ctx.member,
        selfServiceCap: 30, // Juror wants 30 but admin max is 10
        maxAssignmentsOverride: 10,
      },
    }
    const result = resolveEffectiveCap(ctx)
    expect(result.value).toBe(10) // Bounded to admin max
    expect(result.explanation).toContain('bounded')
  })

  it('self-service cap is bounded by group default when no admin override', () => {
    let ctx = withJuryGroup(baseMemberContext(), {
      defaultMaxAssignments: 20,
      allowJurorCapAdjustment: true,
    })
    ctx = { ...ctx, member: { ...ctx.member, selfServiceCap: 50 } }
    const result = resolveEffectiveCap(ctx)
    expect(result.value).toBe(20) // Bounded to group default
  })

  it('self-service cap is ignored when allowJurorCapAdjustment is false', () => {
    let ctx = withJuryGroup(baseMemberContext(), {
      defaultMaxAssignments: 20,
      allowJurorCapAdjustment: false,
    })
    ctx = { ...ctx, member: { ...ctx.member, selfServiceCap: 5 } }
    const result = resolveEffectiveCap(ctx)
    // Should fall through to group default since self-service is disabled
    expect(result.value).toBe(20)
    expect(result.source).toBe('jury_group')
  })
})

// ============================================================================
// resolveEffectiveCapMode
// ============================================================================

describe('resolveEffectiveCapMode', () => {
  it('returns system default (SOFT) when no group', () => {
    const ctx = baseMemberContext()
    const result = resolveEffectiveCapMode(ctx)
    expect(result.value).toBe(SYSTEM_DEFAULT_CAP_MODE)
    expect(result.source).toBe('system')
  })

  it('returns jury group default cap mode', () => {
    const ctx = withJuryGroup(baseMemberContext(), { defaultCapMode: 'HARD' })
    const result = resolveEffectiveCapMode(ctx)
    expect(result.value).toBe('HARD')
    expect(result.source).toBe('jury_group')
  })

  it('admin per-member cap mode override takes precedence', () => {
    let ctx = withJuryGroup(baseMemberContext(), { defaultCapMode: 'SOFT' })
    ctx = { ...ctx, member: { ...ctx.member, capModeOverride: 'NONE' as CapMode } }
    const result = resolveEffectiveCapMode(ctx)
    expect(result.value).toBe('NONE')
    expect(result.source).toBe('member')
  })
})

// ============================================================================
// resolveEffectiveSoftCapBuffer
// ============================================================================

describe('resolveEffectiveSoftCapBuffer', () => {
  it('returns system default (2) when no group', () => {
    const ctx = baseMemberContext()
    const result = resolveEffectiveSoftCapBuffer(ctx)
    expect(result.value).toBe(SYSTEM_DEFAULT_SOFT_BUFFER)
    expect(result.source).toBe('system')
  })

  it('returns jury group buffer', () => {
    const ctx = withJuryGroup(baseMemberContext(), { softCapBuffer: 5 })
    const result = resolveEffectiveSoftCapBuffer(ctx)
    expect(result.value).toBe(5)
    expect(result.source).toBe('jury_group')
  })
})

// ============================================================================
// resolveEffectiveCategoryBias
// ============================================================================

describe('resolveEffectiveCategoryBias', () => {
  it('returns null when no bias configured', () => {
    const ctx = baseMemberContext()
    const result = resolveEffectiveCategoryBias(ctx)
    expect(result.value).toBeNull()
    expect(result.source).toBe('system')
  })

  it('uses admin-set preferredStartupRatio', () => {
    let ctx = baseMemberContext()
    ctx = { ...ctx, member: { ...ctx.member, preferredStartupRatio: 0.7 } }
    const result = resolveEffectiveCategoryBias(ctx)
    expect(result.value!.STARTUP).toBeCloseTo(0.7)
    expect(result.value!.BUSINESS_CONCEPT).toBeCloseTo(0.3)
    expect(result.source).toBe('member')
  })

  it('uses self-service ratio when allowJurorRatioAdjustment is true', () => {
    let ctx = withJuryGroup(baseMemberContext(), {
      allowJurorRatioAdjustment: true,
    })
    ctx = { ...ctx, member: { ...ctx.member, selfServiceRatio: 0.6 } }
    const result = resolveEffectiveCategoryBias(ctx)
    expect(result.value).toEqual({ STARTUP: 0.6, BUSINESS_CONCEPT: 0.4 })
    expect(result.source).toBe('member')
  })

  it('self-service ratio takes precedence over admin ratio', () => {
    let ctx = withJuryGroup(baseMemberContext(), {
      allowJurorRatioAdjustment: true,
    })
    ctx = {
      ...ctx,
      member: { ...ctx.member, selfServiceRatio: 0.8, preferredStartupRatio: 0.5 },
    }
    const result = resolveEffectiveCategoryBias(ctx)
    expect(result.value!.STARTUP).toBe(0.8) // Self-service wins
  })

  it('derives bias from group category quotas', () => {
    const ctx = withJuryGroup(baseMemberContext(), {
      categoryQuotasEnabled: true,
      defaultCategoryQuotas: {
        STARTUP: { min: 5, max: 15 },
        BUSINESS_CONCEPT: { min: 3, max: 5 },
      },
    })
    const result = resolveEffectiveCategoryBias(ctx)
    expect(result.source).toBe('jury_group')
    // 15/(15+5) = 0.75, 5/(15+5) = 0.25
    expect(result.value!.STARTUP).toBe(0.75)
    expect(result.value!.BUSINESS_CONCEPT).toBe(0.25)
  })

  it('ignores group quotas when categoryQuotasEnabled is false', () => {
    const ctx = withJuryGroup(baseMemberContext(), {
      categoryQuotasEnabled: false,
      defaultCategoryQuotas: {
        STARTUP: { min: 5, max: 15 },
        BUSINESS_CONCEPT: { min: 3, max: 5 },
      },
    })
    const result = resolveEffectiveCategoryBias(ctx)
    expect(result.value).toBeNull()
  })
})

// ============================================================================
// evaluateAssignmentPolicy — canAssignMore / remainingCapacity / isOverCap
// ============================================================================

describe('evaluateAssignmentPolicy', () => {
  describe('HARD cap mode', () => {
    it('canAssignMore is true when below cap', () => {
      let ctx = withJuryGroup(baseMemberContext(), {
        defaultMaxAssignments: 10,
        defaultCapMode: 'HARD',
      })
      ctx = { ...ctx, currentAssignmentCount: 7 }
      const result = evaluateAssignmentPolicy(ctx)
      expect(result.canAssignMore).toBe(true)
      expect(result.remainingCapacity).toBe(3)
      expect(result.isOverCap).toBe(false)
    })

    it('canAssignMore is false when at cap', () => {
      let ctx = withJuryGroup(baseMemberContext(), {
        defaultMaxAssignments: 10,
        defaultCapMode: 'HARD',
      })
      ctx = { ...ctx, currentAssignmentCount: 10 }
      const result = evaluateAssignmentPolicy(ctx)
      expect(result.canAssignMore).toBe(false)
      expect(result.remainingCapacity).toBe(0)
    })

    it('detects over-cap with correct count', () => {
      let ctx = withJuryGroup(baseMemberContext(), {
        defaultMaxAssignments: 10,
        defaultCapMode: 'HARD',
      })
      ctx = { ...ctx, currentAssignmentCount: 13 }
      const result = evaluateAssignmentPolicy(ctx)
      expect(result.isOverCap).toBe(true)
      expect(result.overCapBy).toBe(3)
      expect(result.canAssignMore).toBe(false)
    })
  })

  describe('SOFT cap mode', () => {
    it('canAssignMore is true within buffer zone', () => {
      let ctx = withJuryGroup(baseMemberContext(), {
        defaultMaxAssignments: 10,
        defaultCapMode: 'SOFT',
        softCapBuffer: 3,
      })
      ctx = { ...ctx, currentAssignmentCount: 11 } // Over cap but within buffer
      const result = evaluateAssignmentPolicy(ctx)
      expect(result.canAssignMore).toBe(true)
      expect(result.isOverCap).toBe(true) // Over the nominal cap
      expect(result.remainingCapacity).toBe(2) // 10+3-11
    })

    it('canAssignMore is false beyond buffer', () => {
      let ctx = withJuryGroup(baseMemberContext(), {
        defaultMaxAssignments: 10,
        defaultCapMode: 'SOFT',
        softCapBuffer: 3,
      })
      ctx = { ...ctx, currentAssignmentCount: 13 } // cap(10) + buffer(3) = 13 → full
      const result = evaluateAssignmentPolicy(ctx)
      expect(result.canAssignMore).toBe(false)
      expect(result.remainingCapacity).toBe(0)
    })
  })

  describe('NONE cap mode', () => {
    it('canAssignMore is always true', () => {
      let ctx = withJuryGroup(baseMemberContext(), {
        defaultMaxAssignments: 10,
        defaultCapMode: 'NONE',
      })
      ctx = { ...ctx, currentAssignmentCount: 100 }
      const result = evaluateAssignmentPolicy(ctx)
      expect(result.canAssignMore).toBe(true)
      expect(result.remainingCapacity).toBe(Infinity)
    })
  })

  describe('aggregate provenance', () => {
    it('returns all four policy resolutions with correct sources', () => {
      let ctx = withJuryGroup(baseMemberContext(), {
        defaultMaxAssignments: 20,
        defaultCapMode: 'SOFT',
        softCapBuffer: 2,
      })
      ctx = { ...ctx, currentAssignmentCount: 5 }
      const result = evaluateAssignmentPolicy(ctx)

      expect(result.effectiveCap.source).toBe('jury_group')
      expect(result.effectiveCap.value).toBe(20)
      expect(result.effectiveCapMode.source).toBe('jury_group')
      expect(result.effectiveCapMode.value).toBe('SOFT')
      expect(result.softCapBuffer.source).toBe('jury_group')
      expect(result.softCapBuffer.value).toBe(2)
      expect(result.categoryBias.source).toBe('system')
      expect(result.categoryBias.value).toBeNull()
    })
  })
})
