import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Hono } from 'hono'
import type { AppEnv } from '../src/types'

const mockListProducts = vi.fn()
const mockGetProduct = vi.fn()
const mockCreateProduct = vi.fn()
const mockUpdateProduct = vi.fn()
const mockSetProductStockItemId = vi.fn()
const mockSetProductActive = vi.fn()
const mockDeleteProduct = vi.fn()
const mockProductInUse = vi.fn()
const mockProvisionStockItem = vi.fn()
const mockGetProductInventoryHistory = vi.fn()
const mockPostProductOpeningBalance = vi.fn()

const { MockProductNameConflictError } = vi.hoisted(() => {
  class MockProductNameConflictError extends Error {}
  return { MockProductNameConflictError }
})

let mockSession: Record<string, unknown> = {
  type: 'user',
  sub: 'user-1',
  tid: 'tenant-1',
  permissions: ['invoices:read', 'settings:write'],
}

vi.mock('../src/middleware/guards', () => ({
  requirePermission: (permission: string) => {
    return async (
      c: { get: (key: string) => unknown; json: (body: unknown, status: number) => Response },
      next: () => Promise<void>,
    ) => {
      const session = c.get('session') as { permissions?: string[] } | undefined
      if (!session?.permissions?.includes(permission)) {
        return c.json({ error: 'Forbidden' }, 403)
      }
      await next()
    }
  },
}))

vi.mock('../src/middleware/auth', () => ({
  authMiddleware: async (c: { set: (key: string, value: unknown) => void }, next: () => Promise<void>) => {
    c.set('db', {
      transaction: async (callback: (tx: unknown) => Promise<unknown>) => callback({}),
    })
    c.set('session', mockSession)
    await next()
  },
}))

vi.mock('../src/middleware/require-module-enabled', () => ({
  requireModuleEnabled: () => async (_c: unknown, next: () => Promise<void>) => {
    await next()
  },
}))

vi.mock('@zync/db/queries', async () => {
  const actual = await vi.importActual<Record<string, unknown>>('@zync/db/queries')
  return {
    ...actual,
    listProducts: (...args: unknown[]) => mockListProducts(...args),
    getProduct: (...args: unknown[]) => mockGetProduct(...args),
    createProduct: (...args: unknown[]) => mockCreateProduct(...args),
    updateProduct: (...args: unknown[]) => mockUpdateProduct(...args),
    setProductStockItemId: (...args: unknown[]) => mockSetProductStockItemId(...args),
    setProductActive: (...args: unknown[]) => mockSetProductActive(...args),
    deleteProduct: (...args: unknown[]) => mockDeleteProduct(...args),
    productInUse: (...args: unknown[]) => mockProductInUse(...args),
    getProductInventoryHistory: (...args: unknown[]) => mockGetProductInventoryHistory(...args),
    ProductNameConflictError: MockProductNameConflictError,
  }
})

vi.mock('../src/integrations/platform/inventory', () => ({
  provisionStockItem: (...args: unknown[]) => mockProvisionStockItem(...args),
  postProductOpeningBalance: (...args: unknown[]) => mockPostProductOpeningBalance(...args),
}))

import { productsRoute } from '../src/routes/products'

function appWithRoute() {
  const app = new Hono<AppEnv>()
  app.route('/api/products', productsRoute)
  return app
}

const expectDb = expect.objectContaining({
  transaction: expect.any(Function),
})

describe('/api/products routes', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockSession = {
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['invoices:read', 'settings:write'],
    }
    mockListProducts.mockResolvedValue([])
    mockGetProduct.mockResolvedValue(null)
    mockCreateProduct.mockResolvedValue(null)
    mockUpdateProduct.mockResolvedValue(null)
    mockSetProductStockItemId.mockResolvedValue(null)
    mockSetProductActive.mockResolvedValue(null)
    mockDeleteProduct.mockResolvedValue(undefined)
    mockProductInUse.mockResolvedValue(false)
    mockProvisionStockItem.mockResolvedValue(null)
    mockGetProductInventoryHistory.mockResolvedValue(null)
    mockPostProductOpeningBalance.mockResolvedValue(undefined)
  })

  it('lists products for invoice readers', async () => {
    mockListProducts.mockResolvedValue([
      {
        id: 'product-1',
        tenantId: 'tenant-1',
        name: 'Monthly retainer',
        description: null,
        unitPrice: 5000,
        currency: 'ILS',
        unit: 'month',
        category: 'Retainer',
        taxRateId: 'tax-1',
        isTracked: false,
        stockItemId: null,
        isActive: true,
        createdAt: '2026-06-01T00:00:00.000Z',
        updatedAt: '2026-06-01T00:00:00.000Z',
      },
    ])

    const res = await appWithRoute().request('/api/products?q=retainer&limit=5')

    expect(res.status).toBe(200)
    expect(mockListProducts).toHaveBeenCalledWith(
      expectDb,
      'tenant-1',
      expect.objectContaining({ q: 'retainer', limit: 5, includeArchived: false }),
    )
    await expect(res.json()).resolves.toEqual({
      items: [
        expect.objectContaining({ id: 'product-1', name: 'Monthly retainer' }),
      ],
    })
  })

  it('does not expose inventory history to invoice-only readers', async () => {
    mockSession = {
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['invoices:read', 'settings:write'],
    }

    const res = await appWithRoute().request('/api/products/product-1/inventory-history')

    expect(res.status).toBe(403)
    await expect(res.json()).resolves.toMatchObject({ error: 'Forbidden' })
  })

  it('returns inventory history for inventory readers', async () => {
    mockSession = {
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['inventory:read', 'settings:write'],
    }

    mockGetProductInventoryHistory.mockResolvedValue({
      product: {
        id: 'product-1',
        stockItemId: 'stock-1',
        name: 'Tracked widget',
        unit: 'unit',
        currency: 'ILS',
        isActive: true,
      },
      summary: {
        qtyOnHand: 8,
        inventoryValue: 100,
        locationCount: 1,
        lastMovementAt: '2026-07-05T10:00:00.000Z',
      },
      locations: [],
      movements: [],
    })

    const res = await appWithRoute().request('/api/products/product-1/inventory-history')

    expect(res.status).toBe(200)
    expect(mockGetProductInventoryHistory).toHaveBeenCalledWith(expectDb, 'tenant-1', 'product-1')
  })

  it('forbids writes without settings:write', async () => {
    mockSession = {
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['invoices:read'],
    }

    const res = await appWithRoute().request('/api/products', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({
        name: 'Consulting',
        unitPrice: 350,
        currency: 'ILS',
        unit: 'hour',
      }),
    })

    expect(res.status).toBe(403)
    await expect(res.json()).resolves.toMatchObject({ error: 'Forbidden' })
  })

  it('provisions a stock item for tracked products on create', async () => {
    mockCreateProduct.mockResolvedValue({
      id: 'product-2',
      tenantId: 'tenant-1',
      name: 'Tracked widget',
      description: null,
      unitPrice: 99,
      currency: 'ILS',
      unit: 'unit',
      category: null,
      taxRateId: null,
      isTracked: true,
      stockItemId: null,
      isActive: true,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
    })
    mockProvisionStockItem.mockResolvedValue('product-2')
    mockSetProductStockItemId.mockResolvedValue({
      id: 'product-2',
      tenantId: 'tenant-1',
      name: 'Tracked widget',
      description: null,
      unitPrice: 99,
      currency: 'ILS',
      unit: 'unit',
      category: null,
      taxRateId: null,
      isTracked: true,
      stockItemId: 'product-2',
      isActive: true,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
    })

    const res = await appWithRoute().request('/api/products', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({
        name: 'Tracked widget',
        unitPrice: 99,
        currency: 'ILS',
        unit: 'unit',
        isTracked: true,
      }),
    })

    expect(res.status).toBe(201)
    expect(mockCreateProduct).toHaveBeenCalledOnce()
    expect(mockProvisionStockItem).toHaveBeenCalledWith({}, { tenantId: 'tenant-1', stockItemId: 'product-2' })
    expect(mockSetProductStockItemId).toHaveBeenCalledWith({}, 'tenant-1', 'product-2', 'product-2')
    await expect(res.json()).resolves.toMatchObject({
      id: 'product-2',
      isTracked: true,
      stockItemId: 'product-2',
    })
  })

  it('does not accept client-controlled inventory ownership ids', async () => {
    mockCreateProduct.mockResolvedValue({
      id: 'product-safe',
      tenantId: 'tenant-1',
      name: 'Safe tracked widget',
      description: null,
      unitPrice: 99,
      currency: 'ILS',
      unit: 'unit',
      category: null,
      taxRateId: null,
      isTracked: true,
      stockItemId: null,
      isActive: true,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
    })
    mockProvisionStockItem.mockResolvedValue('product-safe')
    mockSetProductStockItemId.mockResolvedValue({
      id: 'product-safe',
      tenantId: 'tenant-1',
      name: 'Safe tracked widget',
      description: null,
      unitPrice: 99,
      currency: 'ILS',
      unit: 'unit',
      category: null,
      taxRateId: null,
      isTracked: true,
      stockItemId: 'product-safe',
      isActive: true,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
    })

    const res = await appWithRoute().request('/api/products', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({
        name: 'Safe tracked widget',
        unitPrice: 99,
        currency: 'ILS',
        unit: 'unit',
        isTracked: true,
        stockItemId: '99999999-9999-4999-8999-999999999999',
      }),
    })

    expect(res.status).toBe(201)
    expect(mockCreateProduct).toHaveBeenCalledWith(
      expectDb,
      'tenant-1',
      expect.not.objectContaining({ stockItemId: expect.anything() }),
    )
    expect(mockProvisionStockItem).toHaveBeenCalledWith({}, {
      tenantId: 'tenant-1',
      stockItemId: 'product-safe',
    })
    expect(mockSetProductStockItemId).toHaveBeenCalledWith({}, 'tenant-1', 'product-safe', 'product-safe')
  })

  it('does not accept client-controlled inventory ownership ids on update', async () => {
    const product = {
      id: 'product-safe',
      tenantId: 'tenant-1',
      name: 'Safe widget',
      description: null,
      unitPrice: 99,
      currency: 'ILS',
      unit: 'unit',
      category: null,
      taxRateId: null,
      isTracked: false,
      stockItemId: null,
      isActive: true,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
    }
    mockGetProduct.mockResolvedValue(product)
    mockUpdateProduct.mockResolvedValue(product)

    const res = await appWithRoute().request('/api/products/product-safe', {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({
        stockItemId: '99999999-9999-4999-8999-999999999999',
      }),
    })

    expect(res.status).toBe(200)
    expect(mockUpdateProduct).toHaveBeenCalledWith(expectDb, 'tenant-1', 'product-safe', {})
  })

  it('posts opening balance when creating a tracked product with initial stock', async () => {
    mockCreateProduct.mockResolvedValue({
      id: 'product-9',
      tenantId: 'tenant-1',
      name: 'Opening stock widget',
      description: null,
      unitPrice: 40,
      currency: 'ILS',
      unit: 'unit',
      category: null,
      taxRateId: null,
      isTracked: true,
      stockItemId: null,
      isActive: true,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
    })
    mockProvisionStockItem.mockResolvedValue('product-9')
    mockSetProductStockItemId.mockResolvedValue({
      id: 'product-9',
      tenantId: 'tenant-1',
      name: 'Opening stock widget',
      description: null,
      unitPrice: 40,
      currency: 'ILS',
      unit: 'unit',
      category: null,
      taxRateId: null,
      isTracked: true,
      stockItemId: 'product-9',
      isActive: true,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
    })

    const res = await appWithRoute().request('/api/products', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({
        name: 'Opening stock widget',
        unitPrice: 40,
        currency: 'ILS',
        unit: 'unit',
        isTracked: true,
        openingBalanceQuantity: 12,
        openingBalanceUnitCost: 8.5,
      }),
    })

    expect(res.status).toBe(201)
    expect(mockPostProductOpeningBalance).toHaveBeenCalledWith({}, {
      tenantId: 'tenant-1',
      itemId: 'product-9',
      quantity: 12,
      unitCost: 8.5,
      holderRef: 'product:product-9:opening-balance',
    })
  })

  it('returns tracked product movement history for inventory readers', async () => {
    mockSession = {
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['inventory:read', 'settings:write'],
    }

    mockGetProductInventoryHistory.mockResolvedValue({
      product: {
        id: 'product-2',
        tenantId: 'tenant-1',
        name: 'Tracked widget',
        description: null,
        unitPrice: 99,
        currency: 'ILS',
        unit: 'unit',
        category: null,
        taxRateId: null,
        isTracked: true,
        stockItemId: 'stock-1',
        isActive: true,
        createdAt: '2026-06-01T00:00:00.000Z',
        updatedAt: '2026-06-01T00:00:00.000Z',
      },
      summary: {
        qtyOnHand: 9,
        inventoryValue: 85.5,
        locationCount: 2,
        lastMovementAt: '2026-07-05T10:00:00.000Z',
      },
      locations: [
        {
          locationId: 'loc-1',
          locationName: 'Main Warehouse',
          locationCode: 'MAIN',
          qtyOnHand: 7,
        },
      ],
      movements: [
        {
          id: 'move-1',
          kind: 'receipt',
          qtyDelta: 12,
          unitCost: 9.5,
          occurredAt: '2026-07-05T10:00:00.000Z',
          locationId: 'loc-1',
          locationName: 'Main Warehouse',
          locationCode: 'MAIN',
          holderRef: 'expense-1',
        },
        {
          id: 'move-2',
          kind: 'sale',
          qtyDelta: -3,
          unitCost: null,
          occurredAt: '2026-07-05T11:00:00.000Z',
          locationId: 'loc-1',
          locationName: 'Main Warehouse',
          locationCode: 'MAIN',
          holderRef: 'invoice-1',
        },
      ],
    })

    const res = await appWithRoute().request('/api/products/product-2/inventory-history')

    expect(res.status).toBe(200)
    expect(mockGetProductInventoryHistory).toHaveBeenCalledWith(expectDb, 'tenant-1', 'product-2')
    await expect(res.json()).resolves.toMatchObject({
      product: expect.objectContaining({ id: 'product-2', stockItemId: 'stock-1' }),
      summary: expect.objectContaining({ qtyOnHand: 9, locationCount: 2 }),
      movements: [
        expect.objectContaining({ id: 'move-1', kind: 'receipt', qtyDelta: 12 }),
        expect.objectContaining({ id: 'move-2', kind: 'sale', qtyDelta: -3 }),
      ],
    })
  })

  it('returns 404 when inventory history is unavailable for the product', async () => {
    mockSession = {
      type: 'user',
      sub: 'user-1',
      tid: 'tenant-1',
      permissions: ['inventory:read', 'settings:write'],
    }

    mockGetProductInventoryHistory.mockResolvedValue(null)

    const res = await appWithRoute().request('/api/products/product-404/inventory-history')

    expect(res.status).toBe(404)
    await expect(res.json()).resolves.toEqual({ error: 'Not found' })
  })

  it('provisions a stock item when a product becomes tracked later', async () => {
    mockGetProduct.mockResolvedValue({
      id: 'product-3',
      tenantId: 'tenant-1',
      name: 'Consulting block',
      description: null,
      unitPrice: 350,
      currency: 'ILS',
      unit: 'hour',
      category: null,
      taxRateId: null,
      isTracked: false,
      stockItemId: null,
      isActive: true,
      createdAt: '2026-06-01T00:00:00.000Z',
      updatedAt: '2026-06-01T00:00:00.000Z',
    })
    mockUpdateProduct.mockResolvedValue({
        id: 'product-3',
        tenantId: 'tenant-1',
        name: 'Consulting block',
        description: null,
        unitPrice: 350,
        currency: 'ILS',
        unit: 'hour',
        category: null,
        taxRateId: null,
        isTracked: true,
        stockItemId: null,
        isActive: true,
        createdAt: '2026-06-01T00:00:00.000Z',
        updatedAt: '2026-06-01T00:00:00.000Z',
      })
    mockSetProductStockItemId.mockResolvedValue({
        id: 'product-3',
        tenantId: 'tenant-1',
        name: 'Consulting block',
        description: null,
        unitPrice: 350,
        currency: 'ILS',
        unit: 'hour',
        category: null,
        taxRateId: null,
        isTracked: true,
        stockItemId: 'product-3',
        isActive: true,
        createdAt: '2026-06-01T00:00:00.000Z',
        updatedAt: '2026-06-01T00:00:00.000Z',
      })
    mockProvisionStockItem.mockResolvedValue('product-3')

    const res = await appWithRoute().request('/api/products/product-3', {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        Origin: 'https://app.zync.is',
      },
      body: JSON.stringify({
        isTracked: true,
      }),
    })

    expect(res.status).toBe(200)
    expect(mockProvisionStockItem).toHaveBeenCalledWith({}, { tenantId: 'tenant-1', stockItemId: 'product-3' })
    expect(mockUpdateProduct).toHaveBeenCalledWith(expectDb, 'tenant-1', 'product-3', { isTracked: true })
    expect(mockSetProductStockItemId).toHaveBeenCalledWith({}, 'tenant-1', 'product-3', 'product-3')
    await expect(res.json()).resolves.toMatchObject({
      id: 'product-3',
      isTracked: true,
      stockItemId: 'product-3',
    })
  })

  it('returns 409 on delete when the product is used in recent invoices', async () => {
    mockProductInUse.mockResolvedValue(true)

    const res = await appWithRoute().request('/api/products/product-1', {
      method: 'DELETE',
      headers: { Origin: 'https://app.zync.is' },
    })

    expect(res.status).toBe(409)
    expect(mockDeleteProduct).not.toHaveBeenCalled()
    await expect(res.json()).resolves.toMatchObject({
      error: expect.stringContaining('Archive'),
    })
  })
})
