import { z } from 'zod'

import { createFieldsDecoder, FieldSelection, selectFields } from './fieldSelection'
import { describe, expect, it } from '@jest/globals'
import { fail } from 'node:assert'

describe('fieldSelection', () => {
  const testSchema = z.object({
    id: z.string(),
    name: z.string(),
    email: z.string(),
    age: z.number(),
  })

  const fieldsDecoder = createFieldsDecoder(testSchema)

  describe('createFieldsDecoder', () => {
    describe('valid selections', () => {
      it('should parse a single field', () => {
        const result = fieldsDecoder.parse('id')
        expect(result).toEqual(['id'])
      })

      it('should parse multiple comma-separated fields', () => {
        const result = fieldsDecoder.parse('id,name,email')
        expect(result).toEqual(['id', 'name', 'email'])
      })

      it('should parse all fields from schema', () => {
        const result = fieldsDecoder.parse('id,name,email,age')
        expect(result).toEqual(['id', 'name', 'email', 'age'])
      })

      it('should handle fields with surrounding whitespace', () => {
        const result = fieldsDecoder.parse('  id  ,  name  ,  email  ')
        expect(result).toEqual(['id', 'name', 'email'])
      })

      it('should handle fields with mixed whitespace', () => {
        const result = fieldsDecoder.parse('id, name,email , age')
        expect(result).toEqual(['id', 'name', 'email', 'age'])
      })
    })

    describe('empty/whitespace handling', () => {
      it('should return undefined for undefined input', () => {
        const result = fieldsDecoder.parse(undefined)
        expect(result).toBeUndefined()
      })

      it('should return undefined for empty string', () => {
        const result = fieldsDecoder.parse('')
        expect(result).toBeUndefined()
      })

      it('should return undefined for whitespace-only string', () => {
        const result = fieldsDecoder.parse('   ')
        expect(result).toBeUndefined()
      })

      it('should filter out empty fields from comma-separated list', () => {
        const result = fieldsDecoder.parse('id,,name,,,email')
        expect(result).toEqual(['id', 'name', 'email'])
      })

      it('should filter out whitespace-only fields', () => {
        const result = fieldsDecoder.parse('id,   ,name')
        expect(result).toEqual(['id', 'name'])
      })

      it('should handle trailing comma', () => {
        const result = fieldsDecoder.parse('id,name,')
        expect(result).toEqual(['id', 'name'])
      })

      it('should handle leading comma', () => {
        const result = fieldsDecoder.parse(',id,name')
        expect(result).toEqual(['id', 'name'])
      })
    })

    describe('invalid fields', () => {
      it('should throw error for invalid field name', () => {
        expect(() => fieldsDecoder.parse('invalid')).toThrow()
      })

      it('should throw error when one field is invalid among valid ones', () => {
        expect(() => fieldsDecoder.parse('id,invalid,name')).toThrow()
      })

      it('should include valid fields in error message', () => {
        try {
          fieldsDecoder.parse('invalid')
          fail('Expected to throw')
        } catch (error) {
          expect(error).toBeInstanceOf(z.ZodError)
          const zodError = error as z.ZodError
          expect(zodError.issues[0].message).toContain('Invalid field "invalid"')
          expect(zodError.issues[0].message).toContain('id')
          expect(zodError.issues[0].message).toContain('name')
          expect(zodError.issues[0].message).toContain('email')
          expect(zodError.issues[0].message).toContain('age')
        }
      })

      it('should reject field names with typos', () => {
        expect(() => fieldsDecoder.parse('Id')).toThrow() // case sensitive
        expect(() => fieldsDecoder.parse('ID')).toThrow()
        expect(() => fieldsDecoder.parse('naem')).toThrow()
      })
    })

    describe('edge cases', () => {
      it('should handle schema with single field', () => {
        const singleFieldSchema = z.object({ onlyField: z.string() })
        const decoder = createFieldsDecoder(singleFieldSchema)

        expect(decoder.parse('onlyField')).toEqual(['onlyField'])
        expect(() => decoder.parse('other')).toThrow()
      })

      it('should handle duplicate fields in input', () => {
        const result = fieldsDecoder.parse('id,id,name,name')
        expect(result).toEqual(['id', 'id', 'name', 'name'])
      })

      it('should preserve field order from input', () => {
        const result = fieldsDecoder.parse('email,id,age,name')
        expect(result).toEqual(['email', 'id', 'age', 'name'])
      })

      it('should work with nested schema (top-level keys only)', () => {
        const nestedSchema = z.object({
          id: z.string(),
          nested: z.object({ inner: z.string() }),
        })
        const decoder = createFieldsDecoder(nestedSchema)

        expect(decoder.parse('id,nested')).toEqual(['id', 'nested'])
        expect(() => decoder.parse('inner')).toThrow()
      })
    })
  })

  describe('selectFields', () => {
    const testData = {
      id: '123',
      name: 'Test User',
      email: 'test@example.com',
      age: 25,
    }

    describe('with field selection', () => {
      it('should select single field', () => {
        const result = selectFields(testData, ['id'] as FieldSelection<typeof testData>)
        expect(result).toEqual({ id: '123' })
      })

      it('should select multiple fields', () => {
        const result = selectFields(testData, ['id', 'name'] as FieldSelection<typeof testData>)
        expect(result).toEqual({ id: '123', name: 'Test User' })
      })

      it('should select all fields when all specified', () => {
        const result = selectFields(testData, ['id', 'name', 'email', 'age'] as FieldSelection<typeof testData>)
        expect(result).toEqual(testData)
      })

      it('should preserve field values exactly', () => {
        const dataWithSpecialValues = {
          id: '',
          name: null as unknown as string,
          count: 0,
          flag: false,
        }
        const result = selectFields(dataWithSpecialValues, ['id', 'name', 'count', 'flag'] as FieldSelection<
          typeof dataWithSpecialValues
        >)
        expect(result).toEqual(dataWithSpecialValues)
      })
    })

    describe('without field selection (returns full object)', () => {
      it('should return full object when fields is undefined', () => {
        const result = selectFields(testData, undefined)
        expect(result).toEqual(testData)
      })

      it('should return full object when fields is empty array', () => {
        const result = selectFields(testData, [] as FieldSelection<typeof testData>)
        expect(result).toEqual(testData)
      })
    })

    describe('edge cases', () => {
      it('should handle empty object', () => {
        const emptyData = {} as Record<string, unknown>
        const result = selectFields(emptyData, ['nonexistent'] as FieldSelection<typeof emptyData>)
        expect(result).toEqual({})
      })

      it('should ignore fields not present in data', () => {
        const partialData = { id: '123', name: 'Test' } as Record<string, unknown>
        const result = selectFields(partialData, ['id', 'name', 'missing'] as FieldSelection<typeof partialData>)
        expect(result).toEqual({ id: '123', name: 'Test' })
      })

      it('should handle data with undefined values', () => {
        const dataWithUndefined = { id: '123', name: undefined }
        const result = selectFields(dataWithUndefined, ['id', 'name'] as FieldSelection<typeof dataWithUndefined>)
        expect(result).toEqual({ id: '123', name: undefined })
        expect('name' in result).toBe(true)
      })

      it('should not modify original object', () => {
        const original = { id: '123', name: 'Test', email: 'test@example.com' }
        const originalCopy = { ...original }
        selectFields(original, ['id'] as FieldSelection<typeof original>)
        expect(original).toEqual(originalCopy)
      })
    })
  })

  describe('integration: createFieldsDecoder + selectFields', () => {
    it('should work together for typical use case', () => {
      const data = { id: '123', name: 'Test', email: 'test@example.com', age: 25 }
      const fields = fieldsDecoder.parse('id,name')
      const result = selectFields(data, fields)
      expect(result).toEqual({ id: '123', name: 'Test' })
    })

    it('should return full object when no fields specified', () => {
      const data = { id: '123', name: 'Test', email: 'test@example.com', age: 25 }
      const fields = fieldsDecoder.parse(undefined)
      const result = selectFields(data, fields)
      expect(result).toEqual(data)
    })

    it('should work with array of objects', () => {
      const dataArray = [
        { id: '1', name: 'First', email: 'first@example.com', age: 20 },
        { id: '2', name: 'Second', email: 'second@example.com', age: 30 },
      ]
      const fields = fieldsDecoder.parse('id,email')
      const results = dataArray.map((item) => selectFields(item, fields))
      expect(results).toEqual([
        { id: '1', email: 'first@example.com' },
        { id: '2', email: 'second@example.com' },
      ])
    })
  })
})
