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

const {
  mockGetSession,
  mockFindFirstProduct,
  mockCreateProduct,
  mockUpdateProduct,
  mockCreateMember,
  mockExecuteRaw,
  mockTransaction,
} = vi.hoisted(() => ({
  mockGetSession: vi.fn(),
  mockFindFirstProduct: vi.fn(),
  mockCreateProduct: vi.fn(),
  mockUpdateProduct: vi.fn(),
  mockCreateMember: vi.fn(),
  mockExecuteRaw: vi.fn().mockResolvedValue(undefined),
  mockTransaction: vi.fn(),
}))

vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/navigation', () => ({ redirect: vi.fn() }))
vi.mock('next/headers', () => ({ cookies: vi.fn().mockResolvedValue({}) }))
vi.mock('iron-session', () => ({
  getIronSession: vi.fn().mockResolvedValue({ userId: 'user-1', isDemo: false }),
}))
vi.mock('@/lib/session', () => ({
  sessionOptions: { cookieName: 'test', password: 'test' },
}))
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
vi.mock('@/lib/product-access', () => ({
  productAccessFilter: vi.fn().mockReturnValue({ OR: [{ user_id: 'user-1' }] }),
}))
vi.mock('@/lib/prisma', () => ({
  prisma: {
    product: { findFirst: mockFindFirstProduct, create: mockCreateProduct, update: mockUpdateProduct },
    productMember: { create: mockCreateMember },
    $executeRaw: mockExecuteRaw,
    $transaction: mockTransaction,
  },
}))

import { createProductAction, updateProductAction } from '@/actions/products'
import { getIronSession } from 'iron-session'

const mockSession = getIronSession as ReturnType<typeof vi.fn>

const SESSION_USER = { userId: 'user-1', isDemo: false }
const SESSION_DEMO = { userId: 'demo-1', isDemo: true }
const PRODUCT_ID = 'product-1'

const VALID_DATA = {
  name: 'Test Product',
  code: 'TP',
  description: 'Een product',
  repo_url: 'https://github.com/org/repo',
  definition_of_done: 'Alles groen',
  auto_pr: false,
}

beforeEach(() => {
  vi.clearAllMocks()
  mockExecuteRaw.mockResolvedValue(undefined)
  mockSession.mockResolvedValue(SESSION_USER)
})

// =============================================================
//  createProductAction
// =============================================================
describe('createProductAction', () => {
  it('happy path: maakt product + member aan en retourneert productId', async () => {
    mockFindFirstProduct.mockResolvedValue(null) // geen dubbele code
    mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise<unknown>) => {
      return fn({
        product: {
          create: vi.fn().mockResolvedValue({ id: PRODUCT_ID }),
        },
        productMember: {
          create: vi.fn().mockResolvedValue({}),
        },
      })
    })

    const result = await createProductAction(VALID_DATA)

    expect(result).toEqual({ success: true, productId: PRODUCT_ID })
  })

  it('demo-user → error', async () => {
    mockSession.mockResolvedValue(SESSION_DEMO)

    const result = await createProductAction(VALID_DATA)

    expect(result).toMatchObject({ error: expect.stringContaining('demo') })
    expect(mockTransaction).not.toHaveBeenCalled()
  })

  it('ongeldige repo_url (niet github) → validatiefout', async () => {
    const result = await createProductAction({ ...VALID_DATA, repo_url: 'https://gitlab.com/org/repo' })

    expect(result).toMatchObject({ error: expect.any(String) })
    expect(mockTransaction).not.toHaveBeenCalled()
  })

  it('dubbele code → error', async () => {
    mockFindFirstProduct.mockResolvedValue({ id: 'other-product' })

    const result = await createProductAction(VALID_DATA)

    expect(result).toMatchObject({
      code: 422,
      fieldErrors: { code: expect.arrayContaining([expect.stringContaining('gebruik')]) },
    })
    expect(mockTransaction).not.toHaveBeenCalled()
  })

  it('naam ontbreekt → validatiefout', async () => {
    const result = await createProductAction({ ...VALID_DATA, name: '' })

    expect(result).toMatchObject({ error: expect.any(String) })
  })
})

// =============================================================
//  updateProductAction
// =============================================================
describe('updateProductAction', () => {
  it('happy path: werkt product bij en stuurt pg_notify', async () => {
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockUpdateProduct.mockResolvedValue({ id: PRODUCT_ID })

    const result = await updateProductAction(PRODUCT_ID, VALID_DATA)

    expect(result).toEqual({ success: true })
    expect(mockUpdateProduct).toHaveBeenCalled()
    expect(mockExecuteRaw).toHaveBeenCalledTimes(1)
  })

  it('demo-user → error', async () => {
    mockSession.mockResolvedValue(SESSION_DEMO)

    const result = await updateProductAction(PRODUCT_ID, VALID_DATA)

    expect(result).toMatchObject({ error: expect.stringContaining('demo') })
    expect(mockUpdateProduct).not.toHaveBeenCalled()
  })

  it('geen toegang tot product → error', async () => {
    mockFindFirstProduct.mockResolvedValue(null)

    const result = await updateProductAction(PRODUCT_ID, VALID_DATA)

    expect(result).toMatchObject({ error: expect.stringContaining('toegang') })
    expect(mockUpdateProduct).not.toHaveBeenCalled()
  })

  it('ongeldige repo_url → validatiefout', async () => {
    const result = await updateProductAction(PRODUCT_ID, { ...VALID_DATA, repo_url: 'https://bitbucket.org/x' })

    expect(result).toMatchObject({ error: expect.any(String) })
    expect(mockUpdateProduct).not.toHaveBeenCalled()
  })
})
