/**
 * Webhook Endpoint API schemas tests
 * Tests validation schemas for webhook endpoint CRUD API
 * Focus: Mass assignment prevention, required field enforcement, partial update rules, type coercion
 */

import { describe, it, expect } from 'vitest'
import {
  CreateWebhookEndpointRequestSchema,
  UpdateWebhookEndpointRequestSchema,
  ListWebhookEndpointsQuerySchema,
  WebhookEndpointResponseSchema
} from '../api-schemas'

const validUuid = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
const validUuid2 = 'b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22'

describe('CreateWebhookEndpointRequestSchema', () => {
  const validPayload = {
    name: 'My Inbound Webhook',
    resourceId: 'my-workflow-workflow'
  }

  describe('valid requests', () => {
    it('accepts name and resourceId', () => {
      const result = CreateWebhookEndpointRequestSchema.parse(validPayload)
      expect(result.name).toBe('My Inbound Webhook')
      expect(result.resourceId).toBe('my-workflow-workflow')
    })

    it('accepts optional description', () => {
      const payload = { ...validPayload, description: 'Receives Stripe events' }
      const result = CreateWebhookEndpointRequestSchema.parse(payload)
      expect(result.description).toBe('Receives Stripe events')
    })

    it('omits description when not provided', () => {
      const result = CreateWebhookEndpointRequestSchema.parse(validPayload)
      expect(result.description).toBeUndefined()
    })

    it('accepts empty string description (description is z.string, not NonEmptyString)', () => {
      const payload = { ...validPayload, description: '' }
      const result = CreateWebhookEndpointRequestSchema.parse(payload)
      expect(result.description).toBe('')
    })
  })

  describe('required fields', () => {
    it('rejects missing name', () => {
      const { name: _name, ...payload } = validPayload
      expect(() => CreateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })

    it('accepts missing resourceId (optional field)', () => {
      const { resourceId: _resourceId, ...payload } = validPayload
      const result = CreateWebhookEndpointRequestSchema.parse(payload)
      expect(result.resourceId).toBeUndefined()
    })

    it('rejects empty name (NonEmptyString enforcement)', () => {
      expect(() => CreateWebhookEndpointRequestSchema.parse({ ...validPayload, name: '' })).toThrow()
    })

    it('rejects empty resourceId (NonEmptyString enforcement)', () => {
      expect(() => CreateWebhookEndpointRequestSchema.parse({ ...validPayload, resourceId: '' })).toThrow()
    })
  })

  describe('SECURITY: mass assignment prevention', () => {
    it('rejects organizationId injection', () => {
      const payload = { ...validPayload, organizationId: 'attacker-org-id' }
      expect(() => CreateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })

    it('rejects id injection (server-generated field)', () => {
      const payload = { ...validPayload, id: validUuid }
      expect(() => CreateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })

    it('rejects key injection (server-generated field)', () => {
      const payload = { ...validPayload, key: 'whk_custom_key' }
      expect(() => CreateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })

    it('rejects status injection', () => {
      const payload = { ...validPayload, status: 'active' }
      expect(() => CreateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })

    it('rejects arbitrary unknown fields', () => {
      const payload = { ...validPayload, malicious: 'value' }
      expect(() => CreateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })
  })
})

describe('UpdateWebhookEndpointRequestSchema', () => {
  describe('valid partial updates', () => {
    it('accepts update with name only', () => {
      const result = UpdateWebhookEndpointRequestSchema.parse({ name: 'New Name' })
      expect(result.name).toBe('New Name')
      expect(result.status).toBeUndefined()
    })

    it('accepts update with status only', () => {
      const result = UpdateWebhookEndpointRequestSchema.parse({ status: 'paused' })
      expect(result.status).toBe('paused')
    })

    it('accepts update with resourceId only', () => {
      const result = UpdateWebhookEndpointRequestSchema.parse({ resourceId: 'other-workflow-workflow' })
      expect(result.resourceId).toBe('other-workflow-workflow')
    })

    it('accepts update with description only', () => {
      const result = UpdateWebhookEndpointRequestSchema.parse({ description: 'Updated desc' })
      expect(result.description).toBe('Updated desc')
    })

    it('accepts update with all fields', () => {
      const payload = {
        name: 'Updated',
        description: 'New desc',
        resourceId: 'new-workflow-workflow',
        status: 'active' as const
      }
      const result = UpdateWebhookEndpointRequestSchema.parse(payload)
      expect(result).toEqual(payload)
    })
  })

  describe('status values', () => {
    it('accepts active status', () => {
      const result = UpdateWebhookEndpointRequestSchema.parse({ status: 'active' })
      expect(result.status).toBe('active')
    })

    it('accepts paused status', () => {
      const result = UpdateWebhookEndpointRequestSchema.parse({ status: 'paused' })
      expect(result.status).toBe('paused')
    })

    it('rejects invalid status value', () => {
      expect(() => UpdateWebhookEndpointRequestSchema.parse({ status: 'disabled' })).toThrow()
    })

    it('rejects empty string status', () => {
      expect(() => UpdateWebhookEndpointRequestSchema.parse({ status: '' })).toThrow()
    })
  })

  describe('at least one field required', () => {
    it('rejects empty object', () => {
      expect(() => UpdateWebhookEndpointRequestSchema.parse({})).toThrow(
        'At least one field (name, description, resourceId, or status) must be provided'
      )
    })

    it('rejects object with all fields explicitly undefined', () => {
      expect(() =>
        UpdateWebhookEndpointRequestSchema.parse({
          name: undefined,
          description: undefined,
          resourceId: undefined,
          status: undefined
        })
      ).toThrow('At least one field')
    })
  })

  describe('SECURITY: mass assignment prevention', () => {
    it('rejects organizationId injection', () => {
      const payload = { name: 'Valid', organizationId: 'attacker-org' }
      expect(() => UpdateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })

    it('rejects id injection', () => {
      const payload = { name: 'Valid', id: validUuid }
      expect(() => UpdateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })

    it('rejects unknown fields', () => {
      const payload = { name: 'Valid', unknownField: 'value' }
      expect(() => UpdateWebhookEndpointRequestSchema.parse(payload)).toThrow()
    })
  })
})

describe('ListWebhookEndpointsQuerySchema', () => {
  describe('defaults', () => {
    it('accepts empty query and applies defaults', () => {
      const result = ListWebhookEndpointsQuerySchema.parse({})
      expect(result.limit).toBe(20)
      expect(result.offset).toBe(0)
      expect(result.status).toBeUndefined()
    })
  })

  describe('status filter', () => {
    it('accepts active status filter', () => {
      const result = ListWebhookEndpointsQuerySchema.parse({ status: 'active' })
      expect(result.status).toBe('active')
    })

    it('accepts paused status filter', () => {
      const result = ListWebhookEndpointsQuerySchema.parse({ status: 'paused' })
      expect(result.status).toBe('paused')
    })

    it('rejects invalid status value', () => {
      expect(() => ListWebhookEndpointsQuerySchema.parse({ status: 'deleted' })).toThrow()
    })
  })

  describe('string coercion (querystring params arrive as strings)', () => {
    it('coerces string limit to number', () => {
      const result = ListWebhookEndpointsQuerySchema.parse({ limit: '50' })
      expect(result.limit).toBe(50)
    })

    it('coerces string offset to number', () => {
      const result = ListWebhookEndpointsQuerySchema.parse({ offset: '100' })
      expect(result.offset).toBe(100)
    })

    it('coerces both limit and offset together', () => {
      const result = ListWebhookEndpointsQuerySchema.parse({ limit: '10', offset: '30' })
      expect(result.limit).toBe(10)
      expect(result.offset).toBe(30)
    })

    it('rejects limit above max (100)', () => {
      expect(() => ListWebhookEndpointsQuerySchema.parse({ limit: '101' })).toThrow()
    })

    it('rejects negative offset', () => {
      expect(() => ListWebhookEndpointsQuerySchema.parse({ offset: '-1' })).toThrow()
    })
  })
})

describe('WebhookEndpointResponseSchema', () => {
  const validResponse = {
    id: validUuid,
    organizationId: validUuid2,
    key: 'whk_abc123def456',
    keyPrefix: 'whk_abc1',
    name: 'Stripe Inbound',
    description: 'Handles Stripe webhook events',
    resourceId: 'stripe-handler-workflow',
    status: 'active' as const,
    lastTriggeredAt: '2026-03-01T12:00:00.000Z',
    requestCount: 42,
    createdAt: '2026-01-01T00:00:00.000Z',
    updatedAt: '2026-03-01T12:00:00.000Z'
  }

  it('accepts a complete valid response object (with key — create response)', () => {
    const result = WebhookEndpointResponseSchema.parse(validResponse)
    expect(result.id).toBe(validUuid)
    expect(result.status).toBe('active')
    expect(result.requestCount).toBe(42)
    expect(result.key).toBe('whk_abc123def456')
  })

  it('accepts a response without key (list/get response — key is optional)', () => {
    const { key: _key, ...responseWithoutKey } = validResponse
    const result = WebhookEndpointResponseSchema.parse(responseWithoutKey)
    expect(result.id).toBe(validUuid)
    expect(result.key).toBeUndefined()
  })

  describe('nullable fields', () => {
    it('accepts null description', () => {
      const result = WebhookEndpointResponseSchema.parse({ ...validResponse, description: null })
      expect(result.description).toBeNull()
    })

    it('accepts null lastTriggeredAt (endpoint never triggered)', () => {
      const result = WebhookEndpointResponseSchema.parse({ ...validResponse, lastTriggeredAt: null })
      expect(result.lastTriggeredAt).toBeNull()
    })

    it('accepts both nullable fields as null', () => {
      const result = WebhookEndpointResponseSchema.parse({
        ...validResponse,
        description: null,
        lastTriggeredAt: null
      })
      expect(result.description).toBeNull()
      expect(result.lastTriggeredAt).toBeNull()
    })
  })

  describe('forward compatibility (not strict)', () => {
    it('accepts extra fields in response (allows API additions without breaking clients)', () => {
      const result = WebhookEndpointResponseSchema.parse({
        ...validResponse,
        newFieldFromFutureApiVersion: 'some-value'
      })
      expect(result.id).toBe(validUuid)
    })
  })

  describe('field validation', () => {
    it('rejects invalid UUID for id', () => {
      expect(() => WebhookEndpointResponseSchema.parse({ ...validResponse, id: 'not-a-uuid' })).toThrow()
    })

    it('rejects invalid UUID for organizationId', () => {
      expect(() => WebhookEndpointResponseSchema.parse({ ...validResponse, organizationId: 'not-a-uuid' })).toThrow()
    })

    it('rejects negative requestCount', () => {
      expect(() => WebhookEndpointResponseSchema.parse({ ...validResponse, requestCount: -1 })).toThrow()
    })

    it('rejects invalid datetime for createdAt', () => {
      expect(() => WebhookEndpointResponseSchema.parse({ ...validResponse, createdAt: 'not-a-date' })).toThrow()
    })

    it('rejects invalid status in response', () => {
      expect(() => WebhookEndpointResponseSchema.parse({ ...validResponse, status: 'unknown' })).toThrow()
    })
  })
})
