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

vi.stubGlobal('setResponseStatus', setResponseStatus)

// --- Mocks ---
const { mockFind, mockFindById, mockFindOneAndDelete, mockExists, mockSaveInstance } = vi.hoisted(() => ({
  mockFind: vi.fn(),
  mockFindById: vi.fn(),
  mockFindOneAndDelete: vi.fn(),
  mockExists: vi.fn(),
  mockSaveInstance: vi.fn(),
}))

// Mock BoardPost as a constructor function with static methods
vi.mock('../../../server/models/boardPost.js', () => {
  function BoardPost(data) {
    Object.assign(this, data)
    this._id = data._id || 'new-post-id'
    this.save = mockSaveInstance
    this.populate = vi.fn().mockResolvedValue(this)
    this.toObject = function () {
      const { save, populate, toObject, deleteOne, ...rest } = this
      return rest
    }
    this.deleteOne = vi.fn().mockResolvedValue({})
  }
  BoardPost.find = mockFind
  BoardPost.findById = mockFindById
  BoardPost.findOneAndDelete = mockFindOneAndDelete
  BoardPost.exists = mockExists
  return { default: BoardPost }
})

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

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

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

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

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

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

const MEMBER_ID = 'member-abc'

function buildFindChain(result) {
  const chain = {
    sort: vi.fn().mockReturnThis(),
    limit: vi.fn().mockReturnThis(),
    populate: vi.fn().mockReturnThis(),
    lean: vi.fn().mockResolvedValue(result),
  }
  mockFind.mockReturnValue(chain)
  return chain
}

describe('GET /api/board/posts', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAuth.mockResolvedValue({ _id: MEMBER_ID })
  })

  it('returns posts sorted by createdAt desc', async () => {
    const posts = [
      { _id: 'p2', title: 'newer', createdAt: '2026-04-02' },
      { _id: 'p1', title: 'older', createdAt: '2026-04-01' },
    ]
    const chain = buildFindChain(posts)

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

    expect(mockFind).toHaveBeenCalledWith({})
    expect(chain.sort).toHaveBeenCalledWith({ createdAt: -1 })
    expect(result).toEqual({ posts })
  })

  it('filters by tag query param', async () => {
    buildFindChain([])
    const event = createMockEvent({ method: 'GET', path: '/api/board/posts?tag=coop' })
    await getHandler(event)
    expect(mockFind).toHaveBeenCalledWith({ tags: 'coop' })
  })

  it('filters by specific author id', async () => {
    buildFindChain([])
    const event = createMockEvent({ method: 'GET', path: '/api/board/posts?author=other-id' })
    await getHandler(event)
    expect(mockFind).toHaveBeenCalledWith({ author: 'other-id' })
  })

  it('filters by author=me using current member id', async () => {
    buildFindChain([])
    const event = createMockEvent({ method: 'GET', path: '/api/board/posts?author=me' })
    await getHandler(event)
    expect(mockFind).toHaveBeenCalledWith({ author: MEMBER_ID })
  })

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

describe('POST /api/board/posts', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAuth.mockResolvedValue({ _id: MEMBER_ID })
    mockSaveInstance.mockResolvedValue(undefined)
  })

  it('creates a post with valid data', async () => {
    validateBody.mockResolvedValue({
      title: 'Looking for co-op advice',
      seeking: 'help with bylaws',
      offering: '',
      note: '',
      tags: ['coop-formation'],
    })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/board/posts',
      body: { title: 'Looking for co-op advice', seeking: 'help with bylaws' },
    })

    const result = await postHandler(event)

    expect(mockSaveInstance).toHaveBeenCalled()
    expect(result.post.title).toBe('Looking for co-op advice')
    expect(result.post.author).toBe(MEMBER_ID)
    expect(result.post.tags).toEqual(['coop-formation'])
  })

  it('rejects when validation fails (both seeking/offering empty)', async () => {
    validateBody.mockRejectedValue(
      createError({ statusCode: 400, statusMessage: 'Validation failed' }),
    )

    const event = createMockEvent({
      method: 'POST',
      path: '/api/board/posts',
      body: { title: 'x' },
    })

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

  it('requires auth (401)', async () => {
    requireAuth.mockRejectedValue(createError({ statusCode: 401, statusMessage: 'Unauthorized' }))
    const event = createMockEvent({
      method: 'POST',
      path: '/api/board/posts',
      body: { title: 'x', seeking: 'y' },
    })
    await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 401 })
  })
})

describe('PATCH /api/board/posts/[id]', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAuth.mockResolvedValue({ _id: MEMBER_ID })
  })

  function mockPost(overrides = {}) {
    const saveFn = vi.fn().mockResolvedValue(undefined)
    const populateFn = vi.fn().mockResolvedValue(undefined)
    const post = {
      _id: 'post-1',
      author: { toString: () => MEMBER_ID },
      title: 'Original',
      seeking: 'need help',
      offering: '',
      note: '',
      tags: [],
      save: saveFn,
      populate: populateFn,
      toObject() {
        return {
          _id: this._id,
          title: this.title,
          seeking: this.seeking,
          offering: this.offering,
          note: this.note,
          tags: this.tags,
        }
      },
      ...overrides,
    }
    mockFindById.mockResolvedValue(post)
    return post
  }

  it('updates own post', async () => {
    const post = mockPost()
    validateBody.mockResolvedValue({ title: 'Updated title' })

    const event = createMockEvent({
      method: 'PATCH',
      path: '/api/board/posts/post-1',
      body: { title: 'Updated title' },
    })
    event.context = { params: { id: 'post-1' } }

    const result = await patchHandler(event)

    expect(post.save).toHaveBeenCalled()
    expect(result.post.title).toBe('Updated title')
  })

  it('rejects editing another members post with 403', async () => {
    mockPost({ author: { toString: () => 'other-member' } })
    validateBody.mockResolvedValue({ title: 'Hack' })

    const event = createMockEvent({
      method: 'PATCH',
      path: '/api/board/posts/post-1',
      body: { title: 'Hack' },
    })
    event.context = { params: { id: 'post-1' } }

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

  it('rejects if merged result has neither seeking nor offering (400)', async () => {
    mockPost({ seeking: 'current', offering: '' })
    validateBody.mockResolvedValue({ seeking: '', offering: '' })

    const event = createMockEvent({
      method: 'PATCH',
      path: '/api/board/posts/post-1',
      body: { seeking: '', offering: '' },
    })
    event.context = { params: { id: 'post-1' } }

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

  it('returns 404 when post not found', async () => {
    mockFindById.mockResolvedValue(null)
    validateBody.mockResolvedValue({ title: 'x' })

    const event = createMockEvent({
      method: 'PATCH',
      path: '/api/board/posts/missing',
      body: { title: 'x' },
    })
    event.context = { params: { id: 'missing' } }

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

describe('DELETE /api/board/posts/[id]', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAuth.mockResolvedValue({ _id: MEMBER_ID })
  })

  it('deletes own post', async () => {
    mockFindOneAndDelete.mockResolvedValue({ _id: 'post-1' })

    const event = createMockEvent({ method: 'DELETE', path: '/api/board/posts/post-1' })
    event.context = { params: { id: 'post-1' } }

    const result = await deleteHandler(event)

    expect(mockFindOneAndDelete).toHaveBeenCalledWith({ _id: 'post-1', author: MEMBER_ID })
    expect(result).toEqual({ success: true })
  })

  it('rejects deleting another members post with 403', async () => {
    mockFindOneAndDelete.mockResolvedValue(null)
    mockExists.mockResolvedValue({ _id: 'post-1' })

    const event = createMockEvent({ method: 'DELETE', path: '/api/board/posts/post-1' })
    event.context = { params: { id: 'post-1' } }

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

  it('returns 404 when post not found', async () => {
    mockFindOneAndDelete.mockResolvedValue(null)
    mockExists.mockResolvedValue(null)

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

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