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

import { requireAuth, requireAdmin } from '../../../server/utils/auth.js'
import { validateBody } from '../../../server/utils/validateBody.js'
import getHandler from '../../../server/api/board/channels.get.js'
import postHandler from '../../../server/api/admin/board-channels.post.js'
import patchHandler from '../../../server/api/admin/board-channels/[id].patch.js'
import deleteHandler from '../../../server/api/admin/board-channels/[id].delete.js'
import { createMockEvent } from '../helpers/createMockEvent.js'

vi.stubGlobal('setResponseStatus', setResponseStatus)

const { mockFind, mockFindOne, mockCreate, mockFindByIdAndUpdate, mockFindByIdAndDelete } = vi.hoisted(() => ({
  mockFind: vi.fn(),
  mockFindOne: vi.fn(),
  mockCreate: vi.fn(),
  mockFindByIdAndUpdate: vi.fn(),
  mockFindByIdAndDelete: vi.fn(),
}))

vi.mock('../../../server/models/boardChannel.js', () => ({
  default: {
    find: mockFind,
    findOne: mockFindOne,
    create: mockCreate,
    findByIdAndUpdate: mockFindByIdAndUpdate,
    findByIdAndDelete: mockFindByIdAndDelete,
  },
}))

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

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

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

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

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

vi.mock('../../../server/utils/slack.ts', () => ({
  getSlackAdminService: () => ({
    createChannel: mockCreateSlackChannel,
  }),
}))

describe('GET /api/board/channels', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAuth.mockResolvedValue({ _id: 'member-1' })
  })

  it('returns channels for authenticated member', async () => {
    const channels = [{ _id: 'c1', name: 'coop' }]
    const chain = {
      sort: vi.fn().mockReturnThis(),
      lean: vi.fn().mockResolvedValue(channels),
    }
    mockFind.mockReturnValue(chain)

    const event = createMockEvent({ method: 'GET', path: '/api/board/channels' })
    const result = await getHandler(event)

    expect(mockFind).toHaveBeenCalledWith({})
    expect(chain.sort).toHaveBeenCalledWith({ name: 1 })
    expect(result).toEqual({ channels })
  })

  it('requires auth (401)', async () => {
    requireAuth.mockRejectedValue(
      createError({ statusCode: 401, statusMessage: 'Unauthorized' }),
    )
    const event = createMockEvent({ method: 'GET', path: '/api/board/channels' })
    await expect(getHandler(event)).rejects.toMatchObject({ statusCode: 401 })
  })
})

describe('POST /api/admin/board-channels', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
    mockFindOne.mockReturnValue({ lean: vi.fn().mockResolvedValue(null) })
  })

  it('creates channel when admin sends valid data', async () => {
    validateBody.mockResolvedValue({
      name: 'coop-formation',
      slackChannelId: 'C01234ABC',
      tagSlugs: ['coop-formation'],
    })
    const created = {
      _id: 'new-channel',
      name: 'coop-formation',
      slackChannelId: 'C01234ABC',
      tagSlugs: ['coop-formation'],
      toObject() {
        return {
          _id: this._id,
          name: this.name,
          slackChannelId: this.slackChannelId,
          tagSlugs: this.tagSlugs,
        }
      },
    }
    mockCreate.mockResolvedValue(created)

    const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
    const result = await postHandler(event)

    expect(mockCreate).toHaveBeenCalledWith({
      name: 'coop-formation',
      slackChannelId: 'C01234ABC',
      tagSlugs: ['coop-formation'],
    })
    expect(result.channel.slackChannelId).toBe('C01234ABC')
  })

  it('rejects missing required fields with 400', async () => {
    validateBody.mockRejectedValue(
      createError({ statusCode: 400, statusMessage: 'Validation failed' }),
    )

    const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
    await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 400 })
  })

  it('rejects non-admin with 403', async () => {
    requireAdmin.mockRejectedValue(
      createError({ statusCode: 403, statusMessage: 'Forbidden' }),
    )

    const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
    await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 403 })
  })

  it('returns 409 on duplicate slackChannelId', async () => {
    validateBody.mockResolvedValue({
      name: 'x',
      slackChannelId: 'C01234ABC',
      tagSlugs: [],
    })
    const dupErr = Object.assign(new Error('dup'), { code: 11000 })
    mockCreate.mockRejectedValue(dupErr)

    const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
    await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 409 })
  })

  it('creates Slack channel via API when slackChannelId not provided', async () => {
    validateBody.mockResolvedValue({
      name: 'coop-formation',
      tagSlugs: [],
    })
    mockCreateSlackChannel.mockResolvedValue({ id: 'C_NEW_123', name: 'coop-formation' })
    const created = {
      _id: 'new-ch',
      name: 'coop-formation',
      slackChannelId: 'C_NEW_123',
      tagSlugs: [],
      toObject() {
        return {
          _id: this._id,
          name: this.name,
          slackChannelId: this.slackChannelId,
          tagSlugs: this.tagSlugs,
        }
      },
    }
    mockCreate.mockResolvedValue(created)

    const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
    const result = await postHandler(event)

    expect(mockCreateSlackChannel).toHaveBeenCalledWith('coop-formation')
    expect(mockCreate).toHaveBeenCalledWith({
      name: 'coop-formation',
      slackChannelId: 'C_NEW_123',
      tagSlugs: [],
    })
    expect(result.channel.slackChannelId).toBe('C_NEW_123')
  })

  it('returns 409 when a tag is already mapped to another channel', async () => {
    validateBody.mockResolvedValue({
      name: 'new-ch',
      slackChannelId: 'C99999',
      tagSlugs: ['coop-formation'],
    })
    mockFindOne.mockReturnValue({
      lean: vi.fn().mockResolvedValue({
        _id: 'existing',
        name: 'old-ch',
        tagSlugs: ['coop-formation'],
      }),
    })

    const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
    await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 409 })
    expect(mockCreate).not.toHaveBeenCalled()
  })
})

describe('PATCH /api/admin/board-channels/[id]', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
    mockFindOne.mockReturnValue({ lean: vi.fn().mockResolvedValue(null) })
  })

  it('updates a channel', async () => {
    validateBody.mockResolvedValue({ name: 'renamed' })
    const updated = {
      _id: 'c1',
      name: 'renamed',
      toObject() {
        return { _id: this._id, name: this.name }
      },
    }
    mockFindByIdAndUpdate.mockResolvedValue(updated)

    const event = createMockEvent({ method: 'PATCH', path: '/api/admin/board-channels/c1' })
    event.context = { params: { id: 'c1' } }

    const result = await patchHandler(event)

    expect(mockFindByIdAndUpdate).toHaveBeenCalledWith(
      'c1',
      { $set: { name: 'renamed' } },
      { new: true, runValidators: true },
    )
    expect(result.channel.name).toBe('renamed')
  })

  it('returns 404 when channel not found', async () => {
    validateBody.mockResolvedValue({ name: 'x' })
    mockFindByIdAndUpdate.mockResolvedValue(null)

    const event = createMockEvent({ method: 'PATCH', path: '/api/admin/board-channels/missing' })
    event.context = { params: { id: 'missing' } }

    await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 404 })
  })

  it('returns 409 when PATCH assigns a tag already owned by another channel', async () => {
    validateBody.mockResolvedValue({ tagSlugs: ['coop-formation'] })
    mockFindOne.mockReturnValue({
      lean: vi.fn().mockResolvedValue({
        _id: 'other',
        name: 'other-ch',
        tagSlugs: ['coop-formation'],
      }),
    })

    const event = createMockEvent({ method: 'PATCH', path: '/api/admin/board-channels/c1' })
    event.context = { params: { id: 'c1' } }

    await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 409 })
    expect(mockFindByIdAndUpdate).not.toHaveBeenCalled()
  })

  it('rejects non-admin with 403', async () => {
    requireAdmin.mockRejectedValue(
      createError({ statusCode: 403, statusMessage: 'Forbidden' }),
    )

    const event = createMockEvent({ method: 'PATCH', path: '/api/admin/board-channels/c1' })
    event.context = { params: { id: 'c1' } }

    await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 403 })
  })
})

describe('DELETE /api/admin/board-channels/[id]', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
  })

  it('deletes a channel', async () => {
    mockFindByIdAndDelete.mockResolvedValue({ _id: 'c1' })

    const event = createMockEvent({ method: 'DELETE', path: '/api/admin/board-channels/c1' })
    event.context = { params: { id: 'c1' } }

    const result = await deleteHandler(event)

    expect(mockFindByIdAndDelete).toHaveBeenCalledWith('c1')
    expect(result).toEqual({ success: true })
  })

  it('returns 404 when channel not found', async () => {
    mockFindByIdAndDelete.mockResolvedValue(null)

    const event = createMockEvent({ method: 'DELETE', path: '/api/admin/board-channels/missing' })
    event.context = { params: { id: 'missing' } }

    await expect(deleteHandler(event)).rejects.toMatchObject({ statusCode: 404 })
  })

  it('rejects non-admin with 403', async () => {
    requireAdmin.mockRejectedValue(
      createError({ statusCode: 403, statusMessage: 'Forbidden' }),
    )

    const event = createMockEvent({ method: 'DELETE', path: '/api/admin/board-channels/c1' })
    event.context = { params: { id: 'c1' } }

    await expect(deleteHandler(event)).rejects.toMatchObject({ statusCode: 403 })
  })
})
