// app/api/wertstrom/route.ts (GET/POST) tests — Wertstrom P0 (KAR-878).
//
// Covers: the new Zod validation (CreateValueStreamMapBody) — title
// non-empty, tolerant null description/project_id, invalid project_id
// rejected — replacing the previous plain `as` cast; and the vsm.read/
// vsm.write Permissions-Enforcement gate (readonly can GET but not POST,
// consultant can do both), replacing the previous getClaims()-only check.
//
// Strategy: mock @/lib/supabase/server (same chainable-query-builder
// approach as app/api/v1/oee/__tests__/route.test.ts) for the DB, and mock
// @/lib/auth/permissions (same approach as
// app/api/admin/__tests__/permission-gates.test.ts) for auth/permissions —
// keeping the real hasPermission (via importOriginal) so the gate is
// genuinely exercised, not stubbed away.

import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { UserSession } from '@/lib/auth/permissions-shared'

interface RecordedInsert { table: string; row: unknown }

function makeSupabaseMock(opts: {
  insertResult?: { data: unknown; error: null | { message: string } }
  /** `.from('projects').select('id').eq('id', ...).maybeSingle()` result —
   * defaults to "found" so existing tests (which pass no project_id, and
   * never reach this lookup) are unaffected. */
  projectLookupResult?: { data: unknown; error: null | { message: string } }
} = {}) {
  const insertResult = opts.insertResult ?? { data: { id: 'vsm-1' }, error: null }
  const projectLookupResult = opts.projectLookupResult ?? { data: { id: 'proj-1' }, error: null }
  const recordedInserts: RecordedInsert[] = []

  function makeQueryChain(table: string) {
    const chain: Record<string, unknown> = {}
    chain.select = vi.fn(() => chain)
    chain.order = vi.fn(() => Promise.resolve({ data: [], error: null }))
    chain.eq = vi.fn(() => chain)
    chain.maybeSingle = vi.fn(() => Promise.resolve(projectLookupResult))

    const insertChain: Record<string, unknown> = {}
    insertChain.select = vi.fn(() => insertChain)
    insertChain.single = vi.fn(() => Promise.resolve(insertResult))
    chain.insert = vi.fn((row: unknown) => {
      recordedInserts.push({ table, row })
      return insertChain
    })
    return chain
  }

  return {
    from: vi.fn((table: string) => makeQueryChain(table)),
    _recordedInserts: recordedInserts,
  }
}

let currentMock = makeSupabaseMock()
let authenticated = true
// Default: both permissions, so tests unrelated to the permission gate
// itself (validation, happy path) are unaffected by which one a given
// handler requires. Dedicated permission tests below override this.
let sessionPermissions: string[] = ['vsm.read', 'vsm.write']

function makeSession(): UserSession {
  return {
    userId: 'user-1',
    authUserId: 'user-1',
    roleId: 'role-1',
    role: 'consultant',
    permissions: sessionPermissions,
    displayName: 'Test User',
    firstName: 'Test',
    lastName: 'User',
    email: 'test@example.com',
    avatarUrl: null,
    departmentId: null,
    mustChangePassword: false,
    accountStatus: 'active',
  }
}

vi.mock('@/lib/supabase/server', () => ({
  createClient: vi.fn(() => Promise.resolve(currentMock)),
}))

vi.mock('@/lib/auth/permissions', async (importOriginal) => {
  const original = await importOriginal<typeof import('@/lib/auth/permissions')>()
  return {
    ...original,
    getUserSession: vi.fn(() => Promise.resolve(authenticated ? makeSession() : null)),
  }
})

beforeEach(() => {
  currentMock = makeSupabaseMock()
  authenticated = true
  sessionPermissions = ['vsm.read', 'vsm.write']
  vi.clearAllMocks()
})

async function getHandler() {
  return import('../route')
}

function makePostRequest(body: unknown) {
  return new Request('http://localhost/api/wertstrom', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
}

describe('GET /api/wertstrom', () => {
  it('returns 401 when no session exists', async () => {
    authenticated = false
    const { GET } = await getHandler()
    const res = await GET()
    expect(res.status).toBe(401)
  })

  it('returns 403 when the session lacks vsm.read', async () => {
    sessionPermissions = ['vsm.write']
    const { GET } = await getHandler()
    const res = await GET()
    expect(res.status).toBe(403)
  })

  it('returns 200 with the (empty) list when authenticated with vsm.read', async () => {
    const { GET } = await getHandler()
    const res = await GET()
    expect(res.status).toBe(200)
    expect(await res.json()).toEqual([])
  })

  it('readonly (vsm.read only) can GET the list', async () => {
    sessionPermissions = ['vsm.read']
    const { GET } = await getHandler()
    const res = await GET()
    expect(res.status).toBe(200)
  })
})

describe('POST /api/wertstrom', () => {
  it('returns 401 when no session exists', async () => {
    authenticated = false
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'Wertstrom Linie 3' }) as never)
    expect(res.status).toBe(401)
  })

  it('returns 403 when the session lacks vsm.write', async () => {
    sessionPermissions = ['vsm.read']
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'Wertstrom Linie 3' }) as never)
    expect(res.status).toBe(403)
    expect(currentMock._recordedInserts).toHaveLength(0)
  })

  // Task acceptance criterion, verbatim: "readonly kann lesen aber nicht
  // schreiben; consultant kann beides" — permission sets exactly as granted
  // by supabase-migration-oee-vsm-permissions.sql.
  it('readonly (vsm.read only) cannot POST (create)', async () => {
    sessionPermissions = ['vsm.read']
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'Wertstrom Linie 3' }) as never)
    expect(res.status).toBe(403)
  })

  it('consultant (vsm.read + vsm.write) can POST (create)', async () => {
    sessionPermissions = ['vsm.read', 'vsm.write']
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'Wertstrom Linie 3' }) as never)
    expect(res.status).toBe(201)
  })

  it('returns 400 for a missing title', async () => {
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({}) as never)
    expect(res.status).toBe(400)
    expect(currentMock._recordedInserts).toHaveLength(0)
  })

  it('returns 400 for an empty/whitespace-only title', async () => {
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: '   ' }) as never)
    expect(res.status).toBe(400)
  })

  it('returns 400 for an invalid project_id (not a UUID)', async () => {
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'x', project_id: 'not-a-uuid' }) as never)
    expect(res.status).toBe(400)
    expect(currentMock._recordedInserts).toHaveLength(0)
  })

  it('creates a Wertstrom with a valid title, defaulting description/project_id to null', async () => {
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'Wertstrom Linie 3' }) as never)
    expect(res.status).toBe(201)
    const row = currentMock._recordedInserts[0].row as Record<string, unknown>
    expect(row.title).toBe('Wertstrom Linie 3')
    expect(row.description).toBeNull()
    expect(row.project_id).toBeNull()
    expect(row.created_by).toBe('user-1')
  })

  it('tolerates an explicit null description/project_id (does not reject)', async () => {
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'x', description: null, project_id: null }) as never)
    expect(res.status).toBe(201)
  })

  // Review-Fix (adversarial review PR #360, C6 Beifang): project_id was only
  // format-validated (uuid), never checked for ownership — the same gap the
  // SimVSM confirm route had (RLS's vsm_own is an OR against created_by,
  // which is always the caller, so the project_id half never actually gets
  // enforced on this insert path).
  it('returns 400 (no insert) for a foreign or non-existent project_id', async () => {
    currentMock = makeSupabaseMock({ projectLookupResult: { data: null, error: null } })
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'x', project_id: '11111111-1111-4111-8111-111111111111' }) as never)
    expect(res.status).toBe(400)
    expect(currentMock._recordedInserts).toHaveLength(0)
  })

  it('creates the Wertstrom (201) when project_id resolves via the RLS-scoped lookup (own project)', async () => {
    const ownProjectId = '22222222-2222-4222-8222-222222222222'
    currentMock = makeSupabaseMock({ projectLookupResult: { data: { id: ownProjectId }, error: null } })
    const { POST } = await getHandler()
    const res = await POST(makePostRequest({ title: 'x', project_id: ownProjectId }) as never)
    expect(res.status).toBe(201)
    const row = currentMock._recordedInserts[0].row as Record<string, unknown>
    expect(row.project_id).toBe(ownProjectId)
  })
})
