import { describe, it, expect, vi, beforeEach } from 'vitest'

const { mockBoardPostExists } = vi.hoisted(() => ({
  mockBoardPostExists: vi.fn()
}))

vi.mock('../../../server/models/boardPost.js', () => ({
  default: { exists: mockBoardPostExists }
}))

vi.mock('../../../server/utils/auth.js', () => ({
  requireAuth: vi.fn()
}))

vi.mock('../../../server/utils/mongoose.js', () => ({
  connectDB: vi.fn()
}))

vi.mock('../../../server/models/member.js', () => ({
  default: { findByIdAndUpdate: vi.fn(), findOneAndUpdate: vi.fn() }
}))

vi.mock('../../../server/utils/validateBody.js', () => ({
  validateBody: vi.fn()
}))

vi.mock('../../../server/utils/schemas.js', () => ({
  onboardingTrackSchema: {}
}))

vi.mock('../../../server/utils/activityLog.js', () => ({
  logActivity: vi.fn()
}))

import { requireAuth } from '../../../server/utils/auth.js'
import { validateBody } from '../../../server/utils/validateBody.js'
import Member from '../../../server/models/member.js'
import { logActivity } from '../../../server/utils/activityLog.js'
import handler from '../../../server/api/onboarding/track.post.js'
import { createMockEvent } from '../helpers/createMockEvent.js'

describe('POST /api/onboarding/track', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAuth.mockResolvedValue({
      _id: 'member-1',
      onboarding: {
        completedAt: null,
        eventPageVisited: false,
        boardPageVisited: false,
        wikiClicked: false,
      },
    })
    Member.findByIdAndUpdate.mockResolvedValue({})
    Member.findOneAndUpdate.mockResolvedValue(null) // no graduation by default
    mockBoardPostExists.mockResolvedValue({ _id: 'post-1' })
  })

  // 2.1: Sets eventPageVisited to true
  it('sets eventPageVisited to true and returns success', async () => {
    validateBody.mockResolvedValue({ goal: 'eventPageVisited' })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { goal: 'eventPageVisited' },
    })

    const result = await handler(event)

    expect(result).toEqual({ success: true, graduated: false })
    expect(Member.findByIdAndUpdate).toHaveBeenCalledWith('member-1', {
      $set: { 'onboarding.eventPageVisited': true },
    })
    expect(logActivity).toHaveBeenCalledWith(
      'member-1',
      'member_onboarding_goal_completed',
      { goal: 'eventPageVisited' },
      { visibility: 'admin' }
    )
  })

  // 2.4: Rejects invalid goal name (400)
  it('rejects an invalid goal name with 400', async () => {
    validateBody.mockRejectedValue(
      createError({ statusCode: 400, statusMessage: 'Validation failed' })
    )

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { goal: 'invalidGoal' },
    })

    await expect(handler(event)).rejects.toMatchObject({ statusCode: 400 })
  })

  // 2.6: Rejects computed goal names like 'hasProfileTags' (400)
  it('rejects computed goal names like hasProfileTags with 400', async () => {
    validateBody.mockRejectedValue(
      createError({ statusCode: 400, statusMessage: 'Validation failed' })
    )

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { goal: 'hasProfileTags' },
    })

    await expect(handler(event)).rejects.toMatchObject({ statusCode: 400 })
  })

  // 2.7: Idempotent — short-circuits when already true
  it('short-circuits without DB write when goal is already true', async () => {
    requireAuth.mockResolvedValue({
      _id: 'member-1',
      onboarding: {
        completedAt: null,
        eventPageVisited: true,
        boardPageVisited: false,
        wikiClicked: false,
      },
    })
    validateBody.mockResolvedValue({ goal: 'eventPageVisited' })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { goal: 'eventPageVisited' },
    })

    const result = await handler(event)

    expect(result).toEqual({ success: true })
    expect(Member.findByIdAndUpdate).not.toHaveBeenCalled()
    expect(Member.findOneAndUpdate).not.toHaveBeenCalled()
    expect(logActivity).not.toHaveBeenCalled()
  })

  // 2.10: Triggers graduation when all four goals met
  it('triggers graduation when all goals are met', async () => {
    validateBody.mockResolvedValue({ goal: 'wikiClicked' })

    // findOneAndUpdate returns a doc when graduation conditions are met
    const graduatedMember = {
      _id: 'member-1',
      onboarding: {
        completedAt: new Date(),
        eventPageVisited: true,
        boardPageVisited: true,
        wikiClicked: true,
      },
    }
    Member.findOneAndUpdate.mockResolvedValue(graduatedMember)

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { goal: 'wikiClicked' },
    })

    const result = await handler(event)

    expect(result).toEqual({ success: true, graduated: true })

    // Should have logged both goal completion and graduation
    expect(logActivity).toHaveBeenCalledTimes(2)
    expect(logActivity).toHaveBeenCalledWith(
      'member-1',
      'member_onboarding_goal_completed',
      { goal: 'wikiClicked' },
      { visibility: 'admin' }
    )
    expect(logActivity).toHaveBeenCalledWith(
      'member-1',
      'member_onboarding_completed',
      {},
      { visibility: 'admin' }
    )
  })

  // 2.12: Graduation no-ops if tags were removed concurrently
  it('does not graduate if tags were removed concurrently', async () => {
    validateBody.mockResolvedValue({ goal: 'wikiClicked' })

    // findOneAndUpdate returns null when conditions aren't met (tags removed)
    Member.findOneAndUpdate.mockResolvedValue(null)

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { goal: 'wikiClicked' },
    })

    const result = await handler(event)

    expect(result).toEqual({ success: true, graduated: false })

    // Should have logged only goal completion, not graduation
    expect(logActivity).toHaveBeenCalledTimes(1)
    expect(logActivity).toHaveBeenCalledWith(
      'member-1',
      'member_onboarding_goal_completed',
      { goal: 'wikiClicked' },
      { visibility: 'admin' }
    )
  })

  // Skip: marks suggestion skipped without touching underlying goal
  it('marks the suggestion skipped when skip is provided', async () => {
    validateBody.mockResolvedValue({ skip: 'wiki' })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { skip: 'wiki' },
    })

    const result = await handler(event)

    expect(result).toEqual({ success: true })
    expect(Member.findByIdAndUpdate).toHaveBeenCalledWith('member-1', {
      $set: { 'onboarding.skipped.wiki': true },
    })
    // No goal log when skipping
    expect(logActivity).not.toHaveBeenCalled()
  })

  it('no-ops skip path when already graduated', async () => {
    requireAuth.mockResolvedValue({
      _id: 'member-1',
      onboarding: { completedAt: new Date() },
    })
    validateBody.mockResolvedValue({ skip: 'board' })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { skip: 'board' },
    })

    const result = await handler(event)

    expect(result).toEqual({ success: true })
    expect(Member.findByIdAndUpdate).not.toHaveBeenCalled()
  })

  // 2.15: Requires auth (401)
  it('returns 401 when not authenticated', async () => {
    requireAuth.mockRejectedValue(
      createError({ statusCode: 401, statusMessage: 'Authentication required' })
    )

    const event = createMockEvent({
      method: 'POST',
      path: '/api/onboarding/track',
      body: { goal: 'eventPageVisited' },
    })

    await expect(handler(event)).rejects.toMatchObject({ statusCode: 401 })
  })
})
