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

const { mockSession } = vi.hoisted(() => ({
  mockSession: { userId: 'user-1', isDemo: false },
}))

vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/headers', () => ({ cookies: vi.fn().mockResolvedValue({}) }))
vi.mock('iron-session', () => ({
  getIronSession: vi.fn().mockImplementation(async () => mockSession),
}))
vi.mock('@/lib/session', () => ({
  sessionOptions: { cookieName: 'test', password: 'test-password-32-chars-minimum-len' },
}))
vi.mock('@/lib/idea-code-server', () => ({
  nextIdeaCode: vi.fn().mockResolvedValue('IDEA-001'),
}))
vi.mock('@/lib/prisma', () => ({
  prisma: {
    idea: {
      create: vi.fn(),
      findFirst: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
    },
    ideaLog: { create: vi.fn() },
    claudeJob: {
      findFirst: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
    },
    claudeWorker: {
      count: vi.fn(),
    },
    pbi: {
      findFirst: vi.fn(),
      findMany: vi.fn(),
      findUnique: vi.fn(),
      create: vi.fn(),
      delete: vi.fn(),
    },
    story: {
      findMany: vi.fn(),
      create: vi.fn(),
    },
    task: {
      findMany: vi.fn(),
      create: vi.fn(),
      count: vi.fn(),
      findUnique: vi.fn().mockResolvedValue(null),
    },
    product: {
      findUnique: vi.fn().mockResolvedValue(null),
    },
    $transaction: vi.fn(),
    $executeRaw: vi.fn().mockResolvedValue(0),
  },
}))

import { prisma } from '@/lib/prisma'
import {
  createIdeaAction,
  updateIdeaAction,
  archiveIdeaAction,
  deleteIdeaAction,
  updateGrillMdAction,
  updatePlanMdAction,
  uploadPlanMdAction,
  downloadIdeaMdAction,
  startGrillJobAction,
  startMakePlanJobAction,
  cancelIdeaJobAction,
  materializeIdeaPlanAction,
  relinkIdeaPlanAction,
} from '@/actions/ideas'

type MockIdea = {
  idea: { create: ReturnType<typeof vi.fn>; findFirst: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> }
  ideaLog: { create: ReturnType<typeof vi.fn> }
  claudeJob: { findFirst: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> }
  claudeWorker: { count: ReturnType<typeof vi.fn> }
  pbi: { findFirst: ReturnType<typeof vi.fn>; findMany: ReturnType<typeof vi.fn>; findUnique: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> }
  story: { findMany: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn> }
  task: { findMany: ReturnType<typeof vi.fn>; create: ReturnType<typeof vi.fn>; count: ReturnType<typeof vi.fn> }
  $transaction: ReturnType<typeof vi.fn>
  $executeRaw: ReturnType<typeof vi.fn>
}
const m = prisma as unknown as MockIdea

beforeEach(() => {
  vi.clearAllMocks()
  mockSession.userId = 'user-1'
  mockSession.isDemo = false
  // Default: $transaction passes its callback through with our mocked prisma
  m.$transaction.mockImplementation(async (arg: unknown) => {
    if (typeof arg === 'function') {
      return (arg as (tx: unknown) => unknown)(m)
    }
    return arg
  })
})

describe('createIdeaAction', () => {
  it('happy path: creates DRAFT idea with auto-generated code', async () => {
    m.idea.create.mockResolvedValueOnce({ id: 'idea-1', code: 'IDEA-001' })

    const r = await createIdeaAction({ title: 'Plant-watering reminder' })
    expect(r).toEqual({ success: true, data: { id: 'idea-1', code: 'IDEA-001' } })
    expect(m.idea.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          user_id: 'user-1',
          code: 'IDEA-001',
          title: 'Plant-watering reminder',
          status: 'DRAFT',
        }),
      }),
    )
  })

  it('rejects unauthenticated', async () => {
    mockSession.userId = ''
    const r = await createIdeaAction({ title: 'x' })
    expect(r).toMatchObject({ error: expect.stringMatching(/ingelogd/), code: 401 })
    expect(m.idea.create).not.toHaveBeenCalled()
  })

  it('rejects demo-user', async () => {
    mockSession.isDemo = true
    const r = await createIdeaAction({ title: 'x' })
    expect(r).toMatchObject({ error: expect.stringMatching(/demo/), code: 403 })
    expect(m.idea.create).not.toHaveBeenCalled()
  })

  it('rejects invalid title (zod 422)', async () => {
    const r = await createIdeaAction({ title: '   ' })
    expect(r).toMatchObject({ code: 422 })
    expect(m.idea.create).not.toHaveBeenCalled()
  })
})

describe('updateIdeaAction', () => {
  it('happy: updates editable idea (DRAFT)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ id: 'idea-1', status: 'DRAFT' })
    m.idea.update.mockResolvedValueOnce({})

    const r = await updateIdeaAction('idea-1', { title: 'Updated' })
    expect(r).toEqual({ success: true })
    expect(m.idea.update).toHaveBeenCalledWith({
      where: { id: 'idea-1' },
      data: { title: 'Updated' },
    })
  })

  it('blocks update on PLANNED (status-mismatch 422)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ id: 'idea-1', status: 'PLANNED' })
    const r = await updateIdeaAction('idea-1', { title: 'x' })
    expect(r).toMatchObject({ code: 422 })
    expect(m.idea.update).not.toHaveBeenCalled()
  })

  it('blocks update during GRILLING', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ id: 'idea-1', status: 'GRILLING' })
    const r = await updateIdeaAction('idea-1', { title: 'x' })
    expect(r).toMatchObject({ code: 422 })
  })

  it('returns 404 when idea belongs to another user', async () => {
    m.idea.findFirst.mockResolvedValueOnce(null)
    const r = await updateIdeaAction('idea-1', { title: 'x' })
    expect(r).toMatchObject({ code: 404 })
  })
})

describe('deleteIdeaAction', () => {
  it('happy: deletes idea without pbi', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ id: 'idea-1', pbi_id: null })
    const r = await deleteIdeaAction('idea-1')
    expect(r).toEqual({ success: true })
    expect(m.idea.delete).toHaveBeenCalledWith({ where: { id: 'idea-1' } })
  })

  it('blocks deletion when PBI is linked', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ id: 'idea-1', pbi_id: 'pbi-1' })
    const r = await deleteIdeaAction('idea-1')
    expect(r).toMatchObject({ code: 422 })
    expect(m.idea.delete).not.toHaveBeenCalled()
  })
})

describe('archiveIdeaAction', () => {
  it('archives owned idea', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ id: 'idea-1' })
    const r = await archiveIdeaAction('idea-1')
    expect(r).toEqual({ success: true })
    expect(m.idea.update).toHaveBeenCalledWith({
      where: { id: 'idea-1' },
      data: { archived: true },
    })
  })
})

describe('updateGrillMdAction', () => {
  it('happy: updates grill_md in GRILLED', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'GRILLED' })
    const r = await updateGrillMdAction('idea-1', '# Updated grill')
    expect(r).toEqual({ success: true })
    expect(m.$transaction).toHaveBeenCalled()
  })

  it('blocks in DRAFT', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'DRAFT' })
    const r = await updateGrillMdAction('idea-1', 'x')
    expect(r).toMatchObject({ code: 422 })
    expect(m.$transaction).not.toHaveBeenCalled()
  })
})

describe('updatePlanMdAction', () => {
  const VALID_PLAN = `---
pbi:
  title: Test
  priority: 2
stories:
  - title: S1
    priority: 2
    tasks:
      - title: T1
        priority: 2
---

body
`

  it('happy: updates plan_md in PLAN_READY with valid yaml', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'PLAN_READY' })
    const r = await updatePlanMdAction('idea-1', VALID_PLAN)
    expect(r).toEqual({ success: true })
  })

  it('rejects invalid yaml (parse-fail 422 with details)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'PLAN_READY' })
    const r = await updatePlanMdAction('idea-1', '# no frontmatter')
    expect(r).toMatchObject({ code: 422 })
    expect((r as { details?: unknown }).details).toBeDefined()
  })

  it('blocks in PLANNED', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'PLANNED' })
    const r = await updatePlanMdAction('idea-1', VALID_PLAN)
    expect(r).toMatchObject({ code: 422 })
  })
})

describe('uploadPlanMdAction', () => {
  const VALID_PLAN = `---
pbi:
  title: Uploaded
  priority: 2
stories:
  - title: S1
    priority: 2
    tasks:
      - title: T1
        priority: 2
---

body
`

  it('happy: uploads from DRAFT — skips grill, sets PLAN_READY', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'DRAFT' })
    const r = await uploadPlanMdAction('idea-1', VALID_PLAN)
    expect(r).toEqual({ success: true })
    expect(m.$transaction).toHaveBeenCalled()
    const txnArg = m.$transaction.mock.calls.at(-1)?.[0] as unknown[] | undefined
    expect(txnArg).toBeDefined()
    // The first call in the transaction is the update — confirm status=PLAN_READY.
    expect(m.idea.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ plan_md: VALID_PLAN, status: 'PLAN_READY' }),
      }),
    )
  })

  it('happy: uploads from GRILLED', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'GRILLED' })
    const r = await uploadPlanMdAction('idea-1', VALID_PLAN)
    expect(r).toEqual({ success: true })
  })

  it('happy: overwrites existing plan from PLAN_READY', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'PLAN_READY' })
    const r = await uploadPlanMdAction('idea-1', VALID_PLAN)
    expect(r).toEqual({ success: true })
  })

  it('happy: uploads from PLAN_FAILED (retry)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'PLAN_FAILED' })
    const r = await uploadPlanMdAction('idea-1', VALID_PLAN)
    expect(r).toEqual({ success: true })
  })

  it('rejects from PLANNED (already materialized)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'PLANNED' })
    const r = await uploadPlanMdAction('idea-1', VALID_PLAN)
    expect(r).toMatchObject({ code: 422 })
    expect(m.$transaction).not.toHaveBeenCalled()
  })

  it('rejects from GRILLING (job running)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'GRILLING' })
    const r = await uploadPlanMdAction('idea-1', VALID_PLAN)
    expect(r).toMatchObject({ code: 422 })
  })

  it('rejects empty markdown', async () => {
    const r = await uploadPlanMdAction('idea-1', '   \n  ')
    expect(r).toMatchObject({ code: 422 })
    // Should fail before touching DB
    expect(m.idea.findFirst).not.toHaveBeenCalled()
  })

  it('rejects oversized markdown', async () => {
    const huge = 'a'.repeat(100_001)
    const r = await uploadPlanMdAction('idea-1', huge)
    expect(r).toMatchObject({ code: 422 })
    expect(m.idea.findFirst).not.toHaveBeenCalled()
  })

  it('rejects invalid yaml (parse-fail 422 with details)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ status: 'DRAFT' })
    const r = await uploadPlanMdAction('idea-1', '# no frontmatter')
    expect(r).toMatchObject({ code: 422 })
    expect((r as { details?: unknown }).details).toBeDefined()
    expect(m.$transaction).not.toHaveBeenCalled()
  })

  it('returns 404 when idea not found', async () => {
    m.idea.findFirst.mockResolvedValueOnce(null)
    const r = await uploadPlanMdAction('nope', VALID_PLAN)
    expect(r).toMatchObject({ code: 404 })
  })
})

describe('startGrillJobAction', () => {
  const idea = {
    id: 'idea-1',
    status: 'DRAFT',
    product_id: 'prod-1',
    product: { id: 'prod-1', repo_url: 'https://github.com/x/y' },
  }

  beforeEach(() => {
    m.idea.findFirst.mockResolvedValue(idea)
    m.claudeJob.findFirst.mockResolvedValue(null)
    m.claudeWorker.count.mockResolvedValue(1)
    m.claudeJob.create.mockResolvedValue({ id: 'job-1' })
  })

  it('happy path: creates IDEA_GRILL job, flips status to GRILLING', async () => {
    const r = await startGrillJobAction('idea-1')
    expect(r).toMatchObject({ success: true, data: { job_id: 'job-1' } })
    expect(m.$executeRaw).toHaveBeenCalled()
  })

  it('blocks demo-user', async () => {
    mockSession.isDemo = true
    const r = await startGrillJobAction('idea-1')
    expect(r).toMatchObject({ code: 403 })
    expect(m.claudeJob.create).not.toHaveBeenCalled()
  })

  it('blocks when product has no repo_url', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      ...idea,
      product: { id: 'prod-1', repo_url: null },
    })
    const r = await startGrillJobAction('idea-1')
    expect(r).toMatchObject({ code: 422, error: expect.stringMatching(/repo_url/i) })
  })

  it('blocks when no idea is unlinked', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ ...idea, product_id: null, product: null })
    const r = await startGrillJobAction('idea-1')
    expect(r).toMatchObject({ code: 422 })
  })

  it('blocks when no worker is active', async () => {
    m.claudeWorker.count.mockResolvedValueOnce(0)
    const r = await startGrillJobAction('idea-1')
    expect(r).toMatchObject({ code: 422, error: expect.stringMatching(/worker/i) })
    expect(m.claudeJob.create).not.toHaveBeenCalled()
  })

  it('blocks when an active job already exists (409)', async () => {
    m.claudeJob.findFirst.mockResolvedValueOnce({ id: 'existing-job' })
    const r = await startGrillJobAction('idea-1')
    expect(r).toMatchObject({ code: 409 })
  })

  it('blocks invalid status (PLANNING)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ ...idea, status: 'PLANNING' })
    const r = await startGrillJobAction('idea-1')
    expect(r).toMatchObject({ code: 422 })
  })
})

describe('startMakePlanJobAction', () => {
  const idea = {
    id: 'idea-1',
    status: 'GRILLED',
    product_id: 'prod-1',
    product: { id: 'prod-1', repo_url: 'https://github.com/x/y' },
  }

  beforeEach(() => {
    m.idea.findFirst.mockResolvedValue(idea)
    m.claudeJob.findFirst.mockResolvedValue(null)
    m.claudeWorker.count.mockResolvedValue(1)
    m.claudeJob.create.mockResolvedValue({ id: 'job-2' })
  })

  it('happy: GRILLED → PLANNING', async () => {
    const r = await startMakePlanJobAction('idea-1')
    expect(r).toMatchObject({ success: true })
  })

  it('blocks from DRAFT (must grill first)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({ ...idea, status: 'DRAFT' })
    const r = await startMakePlanJobAction('idea-1')
    expect(r).toMatchObject({ code: 422 })
  })
})

describe('cancelIdeaJobAction', () => {
  it('grill cancel without prior grill_md → DRAFT', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      id: 'idea-1',
      status: 'GRILLING',
      grill_md: null,
      plan_md: null,
    })
    m.claudeJob.findFirst.mockResolvedValueOnce({ id: 'job-1', kind: 'IDEA_GRILL' })

    const r = await cancelIdeaJobAction('idea-1')
    expect(r).toEqual({ success: true })
    // Verify $transaction was called with 3 ops (job-update, idea-update, log)
    expect(m.$transaction).toHaveBeenCalled()
  })

  it('grill re-grill cancel with prior grill_md → GRILLED', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      id: 'idea-1',
      status: 'GRILLING',
      grill_md: '# old grill',
      plan_md: null,
    })
    m.claudeJob.findFirst.mockResolvedValueOnce({ id: 'job-1', kind: 'IDEA_GRILL' })

    const r = await cancelIdeaJobAction('idea-1')
    expect(r).toEqual({ success: true })
  })

  it('returns 404 when no active job', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      id: 'idea-1',
      status: 'GRILLED',
      grill_md: null,
      plan_md: null,
    })
    m.claudeJob.findFirst.mockResolvedValueOnce(null)
    const r = await cancelIdeaJobAction('idea-1')
    expect(r).toMatchObject({ code: 404 })
  })
})

describe('materializeIdeaPlanAction', () => {
  const VALID_PLAN = `---
pbi:
  title: New PBI
  priority: 2
stories:
  - title: Story A
    priority: 2
    tasks:
      - title: Task A1
        priority: 2
        implementation_plan: "1. Doe X"
      - title: Task A2
        priority: 2
  - title: Story B
    priority: 3
    tasks:
      - title: Task B1
        priority: 3
---

body
`

  beforeEach(() => {
    m.idea.findFirst.mockResolvedValue({
      id: 'idea-1',
      status: 'PLAN_READY',
      product_id: 'prod-1',
      plan_md: VALID_PLAN,
    })
    m.pbi.findMany.mockResolvedValue([])
    m.story.findMany.mockResolvedValue([])
    m.task.findMany.mockResolvedValue([])
    m.pbi.findFirst.mockResolvedValue(null)
    m.pbi.create.mockResolvedValue({ id: 'pbi-1', code: 'PBI-1' })
    m.story.create
      .mockResolvedValueOnce({ id: 's-A' })
      .mockResolvedValueOnce({ id: 's-B' })
    m.task.create
      .mockResolvedValueOnce({ id: 't-A1' })
      .mockResolvedValueOnce({ id: 't-A2' })
      .mockResolvedValueOnce({ id: 't-B1' })
  })

  it('happy: creates PBI + 2 stories + 3 tasks, links idea, returns ids; sort_order = parseCodeNumber(code)', async () => {
    const r = await materializeIdeaPlanAction('idea-1')
    expect(r).toMatchObject({
      success: true,
      data: {
        pbi_id: 'pbi-1',
        pbi_code: 'PBI-1',
        story_ids: ['s-A', 's-B'],
        task_ids: ['t-A1', 't-A2', 't-B1'],
      },
    })
    expect(m.pbi.create).toHaveBeenCalledTimes(1)
    expect(m.story.create).toHaveBeenCalledTimes(2)
    expect(m.task.create).toHaveBeenCalledTimes(3)

    // story sort_order = parseCodeNumber(auto-code): ST-001→1, ST-002→2
    expect(m.story.create.mock.calls[0][0].data.sort_order).toBe(1)
    expect(m.story.create.mock.calls[1][0].data.sort_order).toBe(2)

    // task sort_order = parseCodeNumber(auto-code): T-1→1, T-2→2, T-3→3
    expect(m.task.create.mock.calls[0][0].data.sort_order).toBe(1)
    expect(m.task.create.mock.calls[1][0].data.sort_order).toBe(2)
    expect(m.task.create.mock.calls[2][0].data.sort_order).toBe(3)
  })

  it('blocks when not PLAN_READY (e.g. GRILLED)', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      id: 'idea-1',
      status: 'GRILLED',
      product_id: 'prod-1',
      plan_md: VALID_PLAN,
    })
    const r = await materializeIdeaPlanAction('idea-1')
    expect(r).toMatchObject({ code: 422 })
    expect(m.pbi.create).not.toHaveBeenCalled()
  })

  it('returns 422 with details on parse-fail', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      id: 'idea-1',
      status: 'PLAN_READY',
      product_id: 'prod-1',
      plan_md: '# no frontmatter',
    })
    const r = await materializeIdeaPlanAction('idea-1')
    expect(r).toMatchObject({ code: 422 })
    expect((r as { details?: unknown }).details).toBeDefined()
  })

  it('blocks demo-user', async () => {
    mockSession.isDemo = true
    const r = await materializeIdeaPlanAction('idea-1')
    expect(r).toMatchObject({ code: 403 })
  })

  it('returns 409 on P2002 race', async () => {
    m.$transaction.mockImplementationOnce(async () => {
      throw new Error('Unique constraint failed (P2002)')
    })
    const r = await materializeIdeaPlanAction('idea-1')
    expect(r).toMatchObject({ code: 409 })
  })
})

describe('materializeIdeaPlanAction — existing PBI pre-check', () => {
  const VALID_PLAN = `---
pbi:
  title: New PBI
  priority: 2
stories:
  - title: Story A
    priority: 2
    tasks:
      - title: Task A1
        priority: 2
---

body
`

  beforeEach(() => {
    // Use a distinct userId to avoid sharing the rate-limit bucket with the
    // materializeIdeaPlanAction describe block above.
    mockSession.userId = 'user-precheck'
    m.idea.findFirst.mockResolvedValue({
      id: 'idea-1',
      status: 'PLAN_READY',
      product_id: 'prod-1',
      plan_md: VALID_PLAN,
      pbi_id: 'old-pbi',
    })
    m.pbi.findMany.mockResolvedValue([])
    m.story.findMany.mockResolvedValue([])
    m.task.findMany.mockResolvedValue([])
    m.pbi.findFirst.mockResolvedValue(null)
    m.pbi.findUnique.mockResolvedValue({ code: 'PBI-X' })
    m.pbi.create.mockResolvedValue({ id: 'pbi-new', code: 'PBI-2' })
    m.pbi.delete.mockResolvedValue({})
    m.story.create.mockResolvedValue({ id: 's-1' })
    m.task.create.mockResolvedValue({ id: 't-1' })
  })

  it('auto-vervang: deletes old PBI in transaction when no tasks executed', async () => {
    m.task.count.mockResolvedValueOnce(0)
    const r = await materializeIdeaPlanAction('idea-1')
    expect(r).toMatchObject({ success: true, data: { pbi_id: 'pbi-new' } })
    expect(m.pbi.delete).toHaveBeenCalledWith({ where: { id: 'old-pbi' } })
    expect(m.pbi.create).toHaveBeenCalledTimes(1)
  })

  it('conflict-409: returns PBI_HAS_ACTIVE_TASKS when executed tasks exist', async () => {
    m.task.count.mockResolvedValueOnce(1)
    const r = await materializeIdeaPlanAction('idea-1')
    expect(r).toMatchObject({ code: 409, error: 'PBI_HAS_ACTIVE_TASKS:PBI-X' })
    expect(m.pbi.create).not.toHaveBeenCalled()
    expect(m.pbi.delete).not.toHaveBeenCalled()
  })

  it('alongside: skips old PBI delete and creates new PBI when allowAlongside=true', async () => {
    m.task.count.mockResolvedValueOnce(1)
    const r = await materializeIdeaPlanAction('idea-1', { allowAlongside: true })
    expect(r).toMatchObject({ success: true, data: { pbi_id: 'pbi-new' } })
    expect(m.pbi.delete).not.toHaveBeenCalled()
    expect(m.pbi.create).toHaveBeenCalledTimes(1)
  })
})

describe('relinkIdeaPlanAction', () => {
  it('happy: PLANNED with pbi_id=null → PLAN_READY', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      id: 'idea-1',
      status: 'PLANNED',
      pbi_id: null,
    })
    const r = await relinkIdeaPlanAction('idea-1')
    expect(r).toEqual({ success: true })
    expect(m.$transaction).toHaveBeenCalled()
  })

  it('blocks when pbi still linked', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      id: 'idea-1',
      status: 'PLANNED',
      pbi_id: 'pbi-1',
    })
    const r = await relinkIdeaPlanAction('idea-1')
    expect(r).toMatchObject({ code: 422 })
  })

  it('blocks when not PLANNED', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      id: 'idea-1',
      status: 'PLAN_READY',
      pbi_id: null,
    })
    const r = await relinkIdeaPlanAction('idea-1')
    expect(r).toMatchObject({ code: 422 })
  })
})

describe('downloadIdeaMdAction', () => {
  it('returns grill_md when present', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      code: 'IDEA-001',
      grill_md: '# Idee\nscope',
      plan_md: null,
    })
    const r = await downloadIdeaMdAction('idea-1', 'grill')
    expect(r).toMatchObject({
      success: true,
      data: { filename: 'IDEA-001-grill.md', markdown: '# Idee\nscope' },
    })
  })

  it('404 when md not yet generated', async () => {
    m.idea.findFirst.mockResolvedValueOnce({
      code: 'IDEA-001',
      grill_md: null,
      plan_md: null,
    })
    const r = await downloadIdeaMdAction('idea-1', 'plan')
    expect(r).toMatchObject({ code: 404 })
  })

  it('demo MAY download (read-only operation)', async () => {
    mockSession.isDemo = true
    m.idea.findFirst.mockResolvedValueOnce({
      code: 'IDEA-001',
      grill_md: 'x',
      plan_md: null,
    })
    const r = await downloadIdeaMdAction('idea-1', 'grill')
    expect(r).toMatchObject({ success: true })
  })
})
