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

vi.mock('@/lib/prisma', () => ({
  prisma: {
    product: {
      findMany: vi.fn(),
      findFirst: vi.fn(),
    },
    sprint: {
      findFirst: vi.fn(),
      findUniqueOrThrow: vi.fn(),
      update: vi.fn(),
    },
    story: {
      findFirst: vi.fn(),
      findUniqueOrThrow: vi.fn(),
      findMany: vi.fn(),
      update: vi.fn(),
    },
    task: {
      findFirst: vi.fn(),
      update: vi.fn(),
      findMany: vi.fn(),
    },
    pbi: {
      findUniqueOrThrow: vi.fn(),
      findMany: vi.fn(),
      update: vi.fn(),
    },
    claudeJob: {
      findFirst: vi.fn(),
      updateMany: vi.fn(),
    },
    sprintRun: {
      findUnique: vi.fn(),
      update: vi.fn(),
    },
    storyLog: {
      create: vi.fn(),
    },
    todo: {
      create: vi.fn(),
    },
    $transaction: vi.fn(),
  },
}))

vi.mock('@/lib/api-auth', () => ({
  authenticateApiRequest: vi.fn(),
}))

import { prisma } from '@/lib/prisma'
import { authenticateApiRequest } from '@/lib/api-auth'
import { GET as getProducts } from '@/app/api/products/route'
import { GET as getNextStory } from '@/app/api/products/[id]/next-story/route'
import { GET as getSprintTasks } from '@/app/api/sprints/[id]/tasks/route'
import { POST as postStoryLog } from '@/app/api/stories/[id]/log/route'
import { PATCH as patchTask } from '@/app/api/tasks/[id]/route'

const mockPrisma = prisma as unknown as {
  product: { findMany: ReturnType<typeof vi.fn>; findFirst: ReturnType<typeof vi.fn> }
  sprint: {
    findFirst: ReturnType<typeof vi.fn>
    findUniqueOrThrow: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  story: {
    findFirst: ReturnType<typeof vi.fn>
    findUniqueOrThrow: ReturnType<typeof vi.fn>
    findMany: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  task: {
    findFirst: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
    findMany: ReturnType<typeof vi.fn>
  }
  pbi: {
    findUniqueOrThrow: ReturnType<typeof vi.fn>
    findMany: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  claudeJob: {
    findFirst: ReturnType<typeof vi.fn>
    updateMany: ReturnType<typeof vi.fn>
  }
  sprintRun: {
    findUnique: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  storyLog: { create: ReturnType<typeof vi.fn> }
  todo: { create: ReturnType<typeof vi.fn> }
  $transaction: ReturnType<typeof vi.fn>
}
const mockAuth = authenticateApiRequest as ReturnType<typeof vi.fn>

const UNAUTHORIZED = { error: 'Unauthorized', status: 401 }
const DEMO_AUTH = { userId: 'demo-user', isDemo: true }
const USER_1_AUTH = { userId: 'user-1', isDemo: false }
const USER_2_AUTH = { userId: 'user-2', isDemo: false }

function makeGet(url: string): Request {
  return new Request(url, {
    method: 'GET',
    headers: { Authorization: 'Bearer test-token' },
  })
}

function makePost(url: string, body: unknown): Request {
  return new Request(url, {
    method: 'POST',
    headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
}

function makePatch(url: string, body: unknown): Request {
  return new Request(url, {
    method: 'PATCH',
    headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
}

function routeCtx(id: string) {
  return { params: Promise.resolve({ id }) }
}

beforeEach(() => {
  vi.clearAllMocks()
  // Pass-through transaction so callers can `prisma.$transaction(async tx => ...)` in routes.
  mockPrisma.$transaction.mockImplementation(async (run: unknown) => {
    if (typeof run === 'function') return (run as (tx: typeof prisma) => Promise<unknown>)(prisma)
    return run
  })
})

// ─── GET /api/products ────────────────────────────────────────────────────────

describe('GET /api/products', () => {
  // TC-P-01
  it('returns 401 when no valid token provided', async () => {
    mockAuth.mockResolvedValue(UNAUTHORIZED)
    const res = await getProducts(makeGet('http://localhost/api/products'))
    expect(res.status).toBe(401)
  })

  // TC-P-08
  it('returns only the authenticated user\'s products (cross-user isolation)', async () => {
    mockAuth.mockResolvedValue(USER_1_AUTH)
    mockPrisma.product.findMany.mockResolvedValue([{ id: 'prod-1', name: 'Product A', repo_url: null }])

    const res = await getProducts(makeGet('http://localhost/api/products'))
    const data = await res.json()

    expect(res.status).toBe(200)
    expect(mockPrisma.product.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          archived: false,
          OR: expect.arrayContaining([
            { user_id: 'user-1' },
            { members: { some: { user_id: 'user-1' } } },
          ]),
        }),
      })
    )
    expect(data).toHaveLength(1)
  })
})

// ─── GET /api/products/:id/next-story ────────────────────────────────────────

describe('GET /api/products/:id/next-story', () => {
  // TC-NS-01
  it('returns 401 when no valid token provided', async () => {
    mockAuth.mockResolvedValue(UNAUTHORIZED)
    const res = await getNextStory(
      makeGet('http://localhost/api/products/prod-1/next-story'),
      routeCtx('prod-1')
    )
    expect(res.status).toBe(401)
  })

  // TC-NS-03 / TC-NS-07: product not accessible covers both "not found" and cross-user
  it('returns 404 when product is not accessible to the authenticated user', async () => {
    mockAuth.mockResolvedValue(USER_1_AUTH)
    mockPrisma.sprint.findFirst.mockResolvedValue(null)

    const res = await getNextStory(
      makeGet('http://localhost/api/products/prod-other/next-story'),
      routeCtx('prod-other')
    )
    expect(res.status).toBe(404)
    expect(mockPrisma.sprint.findFirst).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          product_id: 'prod-other',
          status: 'OPEN',
          product: expect.objectContaining({
            OR: expect.arrayContaining([{ user_id: 'user-1' }]),
          }),
        }),
      })
    )
  })

  // TC-NS-07 explicit cross-user
  it('returns 404 for another user\'s product', async () => {
    mockAuth.mockResolvedValue(USER_2_AUTH)
    mockPrisma.sprint.findFirst.mockResolvedValue(null)

    const res = await getNextStory(
      makeGet('http://localhost/api/products/prod-1/next-story'),
      routeCtx('prod-1')
    )
    expect(res.status).toBe(404)
    expect(mockPrisma.sprint.findFirst).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          product: expect.objectContaining({
            OR: expect.arrayContaining([{ user_id: 'user-2' }]),
          }),
        }),
      })
    )
  })
})

// ─── GET /api/sprints/:id/tasks ───────────────────────────────────────────────

describe('GET /api/sprints/:id/tasks', () => {
  // TC-ST-01
  it('returns 401 when no valid token provided', async () => {
    mockAuth.mockResolvedValue(UNAUTHORIZED)
    const res = await getSprintTasks(
      makeGet('http://localhost/api/sprints/sprint-1/tasks'),
      routeCtx('sprint-1')
    )
    expect(res.status).toBe(401)
  })

  // TC-ST-03
  it('returns 404 when sprint is not found', async () => {
    mockAuth.mockResolvedValue(USER_1_AUTH)
    mockPrisma.sprint.findFirst.mockResolvedValue(null)

    const res = await getSprintTasks(
      makeGet('http://localhost/api/sprints/nonexistent/tasks'),
      routeCtx('nonexistent')
    )
    expect(res.status).toBe(404)
  })

  // TC-ST-04
  it('returns 404 for another user\'s sprint', async () => {
    mockAuth.mockResolvedValue(USER_2_AUTH)
    mockPrisma.sprint.findFirst.mockResolvedValue(null)

    const res = await getSprintTasks(
      makeGet('http://localhost/api/sprints/sprint-1/tasks'),
      routeCtx('sprint-1')
    )
    expect(res.status).toBe(404)
    expect(mockPrisma.sprint.findFirst).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          id: 'sprint-1',
          product: expect.objectContaining({
            OR: expect.arrayContaining([{ user_id: 'user-2' }]),
          }),
        }),
      })
    )
  })
})

// ─── POST /api/stories/:id/log ────────────────────────────────────────────────

describe('POST /api/stories/:id/log', () => {
  const VALID_BODY = { type: 'IMPLEMENTATION_PLAN', content: 'Plan: step 1' }

  // TC-L-01
  it('returns 401 when no valid token provided', async () => {
    mockAuth.mockResolvedValue(UNAUTHORIZED)
    const res = await postStoryLog(
      makePost('http://localhost/api/stories/story-1/log', VALID_BODY),
      routeCtx('story-1')
    )
    expect(res.status).toBe(401)
  })

  // TC-L-03
  it('returns 403 for demo users', async () => {
    mockAuth.mockResolvedValue(DEMO_AUTH)
    const res = await postStoryLog(
      makePost('http://localhost/api/stories/story-1/log', VALID_BODY),
      routeCtx('story-1')
    )
    expect(res.status).toBe(403)
    const data = await res.json()
    expect(data.error).toBe('Niet beschikbaar in demo-modus')
  })

  // TC-L-04 / TC-L-05
  it('returns 404 when story is not accessible to the authenticated user', async () => {
    mockAuth.mockResolvedValue(USER_2_AUTH)
    mockPrisma.story.findFirst.mockResolvedValue(null)

    const res = await postStoryLog(
      makePost('http://localhost/api/stories/story-1/log', VALID_BODY),
      routeCtx('story-1')
    )
    expect(res.status).toBe(404)
    expect(mockPrisma.story.findFirst).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          id: 'story-1',
          product: expect.objectContaining({
            OR: expect.arrayContaining([{ user_id: 'user-2' }]),
          }),
        }),
      })
    )
  })
})

// ─── PATCH /api/tasks/:id ─────────────────────────────────────────────────────

describe('PATCH /api/tasks/:id', () => {
  // TC-T-01
  it('returns 401 when no valid token provided', async () => {
    mockAuth.mockResolvedValue(UNAUTHORIZED)
    const res = await patchTask(
      makePatch('http://localhost/api/tasks/task-1', { status: 'DONE' }),
      routeCtx('task-1')
    )
    expect(res.status).toBe(401)
  })

  // TC-T-03
  it('returns 403 for demo users', async () => {
    mockAuth.mockResolvedValue(DEMO_AUTH)
    const res = await patchTask(
      makePatch('http://localhost/api/tasks/task-1', { status: 'DONE' }),
      routeCtx('task-1')
    )
    expect(res.status).toBe(403)
    const data = await res.json()
    expect(data.error).toBeTruthy()
  })

  // TC-T-04
  it('returns 404 when task does not exist', async () => {
    mockAuth.mockResolvedValue(USER_1_AUTH)
    mockPrisma.task.findFirst.mockResolvedValue(null)

    const res = await patchTask(
      makePatch('http://localhost/api/tasks/nonexistent', { status: 'DONE' }),
      routeCtx('nonexistent')
    )
    expect(res.status).toBe(404)
  })

  // TC-T-05
  it('returns 403 when task belongs to a different user', async () => {
    mockAuth.mockResolvedValue(USER_2_AUTH)
    mockPrisma.task.findFirst.mockResolvedValue({
      id: 'task-1',
      story: { product: { user_id: 'user-1' } },
    })

    const res = await patchTask(
      makePatch('http://localhost/api/tasks/task-1', { status: 'DONE' }),
      routeCtx('task-1')
    )
    expect(res.status).toBe(403)
  })

  // TC-T-08 (happy path, sanity check)
  it('returns 200 when task belongs to the authenticated user', async () => {
    mockAuth.mockResolvedValue(USER_1_AUTH)
    mockPrisma.task.findFirst.mockResolvedValue({
      id: 'task-1',
      story: { product: { user_id: 'user-1' } },
    })
    mockPrisma.task.update.mockResolvedValue({
      id: 'task-1',
      title: 'Task',
      status: 'DONE',
      story_id: 'story-1',
      implementation_plan: null,
    })
    mockPrisma.task.findMany.mockResolvedValue([{ status: 'DONE' }])
    mockPrisma.story.findUniqueOrThrow.mockResolvedValue({
      id: 'story-1',
      status: 'DONE',
      pbi_id: 'pbi-1',
      sprint_id: null,
    })
    mockPrisma.story.findMany.mockResolvedValue([{ status: 'DONE' }])
    mockPrisma.pbi.findUniqueOrThrow.mockResolvedValue({ id: 'pbi-1', status: 'DONE' })

    const res = await patchTask(
      makePatch('http://localhost/api/tasks/task-1', { status: 'done' }),
      routeCtx('task-1')
    )
    expect(res.status).toBe(200)
  })
})
