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

import WikiArticle from '../../../server/models/wikiArticle.js'
import { fetchAllDocuments, extractSummary } from '../../../server/utils/outline.js'
import syncHandler from '../../../server/api/admin/wiki/sync.post.js'
import { createMockEvent } from '../helpers/createMockEvent.js'

// Also test extractSummary directly (unmocked)
import { extractSummary as realExtractSummary } from '../../../server/utils/outline.js'

// Mock dependencies before imports
vi.mock('../../../server/models/wikiArticle.js', () => ({
  default: {
    find: vi.fn(),
    findOneAndUpdate: vi.fn()
  }
}))
vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() }))
vi.mock('../../../server/utils/outline.js', () => ({
  fetchAllDocuments: vi.fn(),
  fetchCollections: vi.fn(() => new Map()),
  extractSummary: vi.fn((text) => text || '')
}))

function makeOutlineDoc(overrides = {}) {
  return {
    id: 'doc-1',
    title: 'Test Article',
    url: '/doc/test-article',
    text: 'Some article content',
    publishedAt: '2026-01-15T00:00:00Z',
    createdAt: '2026-01-10T00:00:00Z',
    updatedAt: '2026-01-15T00:00:00Z',
    permission: 'read',
    collection: { name: 'General' },
    ...overrides
  }
}

describe('wiki sync endpoint', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
    WikiArticle.find.mockResolvedValue([])
  })

  it('requires admin auth (403)', async () => {
    requireAdmin.mockRejectedValue(
      createError({ statusCode: 403, statusMessage: 'Admin access required' })
    )

    const event = createMockEvent({
      method: 'POST',
      path: '/api/admin/wiki/sync'
    })

    await expect(syncHandler(event)).rejects.toMatchObject({
      statusCode: 403,
      statusMessage: 'Admin access required'
    })

    expect(requireAdmin).toHaveBeenCalledWith(event)
    expect(fetchAllDocuments).not.toHaveBeenCalled()
  })

  it('upserts new articles from Outline', async () => {
    const docs = [
      makeOutlineDoc({ id: 'doc-1', title: 'First Article' }),
      makeOutlineDoc({ id: 'doc-2', title: 'Second Article' })
    ]
    fetchAllDocuments.mockResolvedValue(docs)
    WikiArticle.find.mockResolvedValue([])
    WikiArticle.findOneAndUpdate.mockResolvedValue({
      lastErrorObject: { updatedExisting: false }
    })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/admin/wiki/sync'
    })

    const result = await syncHandler(event)

    expect(result.created).toBe(2)
    expect(result.updated).toBe(0)
    expect(result.deleted).toBe(0)
    expect(result.errors).toBe(0)
    expect(WikiArticle.findOneAndUpdate).toHaveBeenCalledTimes(2)
    expect(WikiArticle.findOneAndUpdate).toHaveBeenCalledWith(
      { outlineId: 'doc-1' },
      expect.objectContaining({
        $set: expect.objectContaining({ title: 'First Article' })
      }),
      { upsert: true, new: true, rawResult: true }
    )
  })

  it('updates changed titles and URLs', async () => {
    const docs = [
      makeOutlineDoc({ id: 'doc-1', title: 'Updated Title', url: '/doc/new-url' })
    ]
    fetchAllDocuments.mockResolvedValue(docs)
    WikiArticle.find.mockResolvedValue([
      { outlineId: 'doc-1', publishedAt: new Date('2026-01-15') }
    ])
    WikiArticle.findOneAndUpdate.mockResolvedValue({
      lastErrorObject: { updatedExisting: true }
    })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/admin/wiki/sync'
    })

    const result = await syncHandler(event)

    expect(result.updated).toBe(1)
    expect(result.created).toBe(0)
    expect(WikiArticle.findOneAndUpdate).toHaveBeenCalledWith(
      { outlineId: 'doc-1' },
      expect.objectContaining({
        $set: expect.objectContaining({
          title: 'Updated Title',
          url: 'https://wiki.ghostguild.org/doc/new-url'
        })
      }),
      { upsert: true, new: true, rawResult: true }
    )
  })

  it('soft-deletes removed articles (publishedAt set to null)', async () => {
    fetchAllDocuments.mockResolvedValue([
      makeOutlineDoc({ id: 'doc-1' })
    ])
    WikiArticle.find.mockResolvedValue([
      { outlineId: 'doc-1', publishedAt: new Date('2026-01-15') },
      { outlineId: 'doc-2', publishedAt: new Date('2026-01-10') }
    ])
    WikiArticle.findOneAndUpdate.mockResolvedValue({
      lastErrorObject: { updatedExisting: true }
    })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/admin/wiki/sync'
    })

    const result = await syncHandler(event)

    expect(result.deleted).toBe(1)
    // The second call should be the soft-delete for doc-2
    const softDeleteCall = WikiArticle.findOneAndUpdate.mock.calls.find(
      (call) => call[0].outlineId === 'doc-2'
    )
    expect(softDeleteCall).toBeTruthy()
    expect(softDeleteCall[1].$set.publishedAt).toBeNull()
  })

  it('restores re-published articles', async () => {
    const docs = [
      makeOutlineDoc({
        id: 'doc-1',
        publishedAt: '2026-02-01T00:00:00Z'
      })
    ]
    fetchAllDocuments.mockResolvedValue(docs)
    // doc-1 was previously soft-deleted (publishedAt: null)
    WikiArticle.find.mockResolvedValue([
      { outlineId: 'doc-1', publishedAt: null }
    ])
    WikiArticle.findOneAndUpdate.mockResolvedValue({
      lastErrorObject: { updatedExisting: true }
    })

    const event = createMockEvent({
      method: 'POST',
      path: '/api/admin/wiki/sync'
    })

    const result = await syncHandler(event)

    expect(result.updated).toBe(1)
    expect(result.deleted).toBe(0)
    // The upsert should set publishedAt from the Outline document
    const upsertCall = WikiArticle.findOneAndUpdate.mock.calls.find(
      (call) => call[0].outlineId === 'doc-1'
    )
    expect(upsertCall[1].$set.publishedAt).toEqual(new Date('2026-02-01T00:00:00Z'))
  })

  it('aborts on mid-pagination API error with no partial writes', async () => {
    fetchAllDocuments.mockRejectedValue(
      createError({ statusCode: 502, statusMessage: 'Outline API error' })
    )

    const event = createMockEvent({
      method: 'POST',
      path: '/api/admin/wiki/sync'
    })

    await expect(syncHandler(event)).rejects.toMatchObject({
      statusCode: 502
    })

    // No DB writes should have occurred
    expect(WikiArticle.findOneAndUpdate).not.toHaveBeenCalled()
    expect(WikiArticle.find).not.toHaveBeenCalled()
  })

  it('returns counts in response', async () => {
    const docs = [
      makeOutlineDoc({ id: 'doc-1' }),
      makeOutlineDoc({ id: 'doc-2' }),
      makeOutlineDoc({ id: 'doc-3' })
    ]
    fetchAllDocuments.mockResolvedValue(docs)
    WikiArticle.find.mockResolvedValue([
      { outlineId: 'doc-1', publishedAt: new Date() },
      { outlineId: 'doc-4', publishedAt: new Date() }
    ])
    // doc-1 exists (update), doc-2 and doc-3 are new (create), doc-4 is gone (delete)
    WikiArticle.findOneAndUpdate
      .mockResolvedValueOnce({ lastErrorObject: { updatedExisting: true } })  // doc-1 update
      .mockResolvedValueOnce({ lastErrorObject: { updatedExisting: false } }) // doc-2 create
      .mockResolvedValueOnce({ lastErrorObject: { updatedExisting: false } }) // doc-3 create
      .mockResolvedValue({}) // doc-4 soft-delete

    const event = createMockEvent({
      method: 'POST',
      path: '/api/admin/wiki/sync'
    })

    const result = await syncHandler(event)

    expect(result).toEqual({
      created: 2,
      updated: 1,
      deleted: 1,
      errors: 0
    })
  })
})

// Test extractSummary directly — reimport unmocked
describe('extractSummary', () => {
  // We need the real implementation, so we dynamically import it
  // to bypass the module mock above
  let realExtract

  beforeEach(async () => {
    const mod = await vi.importActual('../../../server/utils/outline.js')
    realExtract = mod.extractSummary
  })

  it('returns empty string for falsy input', () => {
    expect(realExtract('')).toBe('')
    expect(realExtract(null)).toBe('')
    expect(realExtract(undefined)).toBe('')
  })

  it('strips HTML tags', () => {
    expect(realExtract('<p>Hello <strong>world</strong></p>')).toBe('Hello world')
  })

  it('returns text as-is when <= 200 chars after stripping', () => {
    const short = 'A short summary.'
    expect(realExtract(short)).toBe(short)
  })

  it('truncates at word boundary without mid-word cuts', () => {
    // Build a string that is longer than 200 chars
    const words = 'word '.repeat(50).trim() // 249 chars: "word word word..."
    const result = realExtract(words)
    expect(result.length).toBeLessThanOrEqual(200)
    // Should not end mid-word
    expect(result).not.toMatch(/\S$\S/)
    // Should end with a complete word
    expect(result.endsWith('word')).toBe(true)
  })
})
