import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { ValidateParams, ValidateParam, ZodValidationError } from './zod-validate.decorator';

describe('Zod Validation Decorators', () => {
  describe('ValidateParam Decorator', () => {
    class TestService {
      callCount = 0;
      lastArgs: any[] = [];

      @ValidateParam(0, z.string())
      singleStringParam(value: string): string {
        this.callCount++;
        this.lastArgs = [value];
        return value.toUpperCase();
      }

      @ValidateParam(0, z.string().email(), 'email')
      emailValidation(email: string): string {
        this.callCount++;
        this.lastArgs = [email];
        return email;
      }

      @ValidateParam(0, z.string().min(1), 'name')
      @ValidateParam(1, z.number().int().positive(), 'age')
      multipleValidations(name: string, age: number): string {
        this.callCount++;
        this.lastArgs = [name, age];
        return `${name} is ${age} years old`;
      }
    }

    let service: TestService;

    beforeEach(() => {
      service = new TestService();
    });

    it('should validate string parameter successfully', () => {
      const result = service.singleStringParam('hello');

      expect(result).toBe('HELLO');
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['hello']);
    });

    it('should throw validation error for invalid string', () => {
      expect(() => service.singleStringParam(123 as any)).toThrow(ZodValidationError);

      try {
        service.singleStringParam(123 as any);
      } catch (error) {
        if (error instanceof ZodValidationError) {
          expect(error.parameterIndex).toBe(0);
          expect(error.parameterName).toBeUndefined();
          expect(error.zodError).toBeInstanceOf(z.ZodError);
          expect(error.message).toContain('Parameter validation failed for parameter at index 0');
        }
      }

      expect(service.callCount).toBe(0);
    });

    it('should validate email parameter successfully', () => {
      const email = 'test@example.com';
      const result = service.emailValidation(email);

      expect(result).toBe(email);
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual([email]);
    });

    it('should throw validation error for invalid email with custom parameter name', () => {
      expect(() => service.emailValidation('invalid-email')).toThrow(ZodValidationError);

      try {
        service.emailValidation('invalid-email');
      } catch (error) {
        if (error instanceof ZodValidationError) {
          expect(error.parameterIndex).toBe(0);
          expect(error.parameterName).toBe('email');
          expect(error.message).toContain('Parameter validation failed for parameter "email"');
          expect(error.zodError.issues[0].code).toBe('invalid_string');
        }
      }

      expect(service.callCount).toBe(0);
    });

    it('should validate multiple parameters with chained decorators', () => {
      const result = service.multipleValidations('John', 25);

      expect(result).toBe('John is 25 years old');
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['John', 25]);
    });

    it('should throw validation error for invalid first parameter with chained decorators', () => {
      expect(() => service.multipleValidations('', 25)).toThrow(ZodValidationError);

      try {
        service.multipleValidations('', 25);
      } catch (error) {
        if (error instanceof ZodValidationError) {
          expect(error.parameterIndex).toBe(0);
          expect(error.parameterName).toBe('name');
          expect(error.message).toContain('Parameter validation failed for parameter "name"');
        }
      }

      expect(service.callCount).toBe(0);
    });
  });

  describe('ValidateParams Decorator', () => {
    interface User {
      name: string;
      age: number;
      email?: string;
    }

    const UserSchema = z.object({
      name: z.string().min(1),
      age: z.number().int().min(0).max(120),
      email: z.string().email().optional(),
    });

    class UserService {
      callCount = 0;
      lastArgs: any[] = [];

      @ValidateParams([z.string().min(1), z.number().int().positive()])
      multipleParams(name: string, age: number): string {
        this.callCount++;
        this.lastArgs = [name, age];
        return `${name} is ${age} years old`;
      }

      @ValidateParams([UserSchema], ['userData'])
      processUser(user: User): string {
        this.callCount++;
        this.lastArgs = [user];
        return `Processing ${user.name}`;
      }

      @ValidateParams(
        [UserSchema, z.boolean().default(false)],
        ['userData', 'sendWelcomeEmail']
      )
      createUser(userData: User, sendEmail?: boolean): string {
        this.callCount++;
        this.lastArgs = [userData, sendEmail];
        return `User ${userData.name} created`;
      }

      @ValidateParams([z.array(z.string().min(1)), z.string().optional()])
      processStrings(items: string[], label?: string): string {
        this.callCount++;
        this.lastArgs = [items, label];
        return `${label || 'Items'}: ${items.join(', ')}`;
      }

      @ValidateParams([z.string().transform(str => str.toUpperCase())])
      transformString(input: string): string {
        this.callCount++;
        this.lastArgs = [input];
        return `Transformed: ${input}`;
      }

      @ValidateParams([z.string().transform(str => new Date(str))])
      processDate(dateString: string): string {
        this.callCount++;
        this.lastArgs = [dateString];
        const date = dateString as any; // After transformation, it's a Date
        return date.toISOString();
      }
    }

    let service: UserService;

    beforeEach(() => {
      service = new UserService();
    });

    it('should validate multiple parameters successfully', () => {
      const result = service.multipleParams('John', 25);

      expect(result).toBe('John is 25 years old');
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['John', 25]);
    });

    it('should throw validation error for invalid first parameter', () => {
      expect(() => service.multipleParams('', 25)).toThrow(ZodValidationError);

      try {
        service.multipleParams('', 25);
      } catch (error) {
        if (error instanceof ZodValidationError) {
          expect(error.parameterIndex).toBe(0);
          expect(error.message).toContain('Parameter validation failed for parameter at index 0');
        }
      }

      expect(service.callCount).toBe(0);
    });

    it('should throw validation error for invalid second parameter', () => {
      expect(() => service.multipleParams('John', -5)).toThrow(ZodValidationError);

      try {
        service.multipleParams('John', -5);
      } catch (error) {
        if (error instanceof ZodValidationError) {
          expect(error.parameterIndex).toBe(1);
          expect(error.message).toContain('Parameter validation failed for parameter at index 1');
        }
      }

      expect(service.callCount).toBe(0);
    });

    it('should validate valid user object', () => {
      const user = { name: 'John', age: 25, email: 'john@example.com' };
      const result = service.processUser(user);

      expect(result).toBe('Processing John');
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual([user]);
    });

    it('should validate user object without optional email', () => {
      const user = { name: 'Jane', age: 30 };
      const result = service.processUser(user);

      expect(result).toBe('Processing Jane');
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual([user]);
    });

    it('should throw validation error for missing required field with custom parameter name', () => {
      const invalidUser = { age: 25 } as any;

      expect(() => service.processUser(invalidUser)).toThrow(ZodValidationError);

      try {
        service.processUser(invalidUser);
      } catch (error) {
        if (error instanceof ZodValidationError) {
          expect(error.parameterIndex).toBe(0);
          expect(error.parameterName).toBe('userData');
          expect(error.message).toContain('Parameter validation failed for parameter "userData"');
          expect(error.zodError.issues[0].path).toContain('name');
          expect(error.zodError.issues[0].code).toBe('invalid_type');
        }
      }

      expect(service.callCount).toBe(0);
    });

    it('should handle default values in schemas', () => {
      const user = { name: 'John', age: 25 };
      const result = service.createUser(user);

      expect(result).toBe('User John created');
      expect(service.callCount).toBe(1);
      // Second parameter should be set to false (default value)
      expect(service.lastArgs).toEqual([user, false]);
    });

    it('should validate array of strings', () => {
      const items = ['hello', 'world', 'test'];
      const result = service.processStrings(items);

      expect(result).toBe('Items: hello, world, test');
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual([items, undefined]);
    });

    it('should throw validation error for empty string in array', () => {
      const items = ['hello', '', 'world'];

      expect(() => service.processStrings(items)).toThrow(ZodValidationError);

      try {
        service.processStrings(items);
      } catch (error) {
        if (error instanceof ZodValidationError) {
          expect(error.parameterIndex).toBe(0);
          expect(error.zodError.issues[0].path).toEqual([1]);
          expect(error.zodError.issues[0].code).toBe('too_small');
        }
      }

      expect(service.callCount).toBe(0);
    });

    it('should handle transformations', () => {
      const result = service.transformString('hello');

      expect(result).toBe('Transformed: HELLO');
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['HELLO']);
    });

    it('should transform string to date', () => {
      const dateString = '2023-01-01';
      const result = service.processDate(dateString);

      expect(result).toBe('2023-01-01T00:00:00.000Z');
      expect(service.callCount).toBe(1);
      // The argument should be transformed to a Date object
      expect(service.lastArgs[0]).toBeInstanceOf(Date);
    });
  });

  describe('Async Methods', () => {
    class AsyncService {
      callCount = 0;
      lastArgs: any[] = [];

      @ValidateParam(0, z.string().min(1), 'data')
      async processDataAsync(data: string): Promise<string> {
        this.callCount++;
        this.lastArgs = [data];
        return Promise.resolve(`Processed: ${data}`);
      }

      @ValidateParams([z.string().uuid(), z.boolean().optional()], ['userId', 'includeDetails'])
      async fetchUserAsync(id: string, includeDetails?: boolean): Promise<any> {
        this.callCount++;
        this.lastArgs = [id, includeDetails];
        return Promise.resolve({ id, includeDetails });
      }
    }

    let service: AsyncService;

    beforeEach(() => {
      service = new AsyncService();
    });

    it('should validate parameters for async methods', async () => {
      const result = await service.processDataAsync('test data');

      expect(result).toBe('Processed: test data');
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual(['test data']);
    });

    it('should throw validation error for async methods', async () => {
      let caughtError: ZodValidationError | null = null;
      try {
        await service.processDataAsync('');
      } catch (error) {
        if (error instanceof ZodValidationError) {
          caughtError = error;
        }
      }

      expect(caughtError).not.toBeNull();
      expect(caughtError!.parameterIndex).toBe(0);
      expect(caughtError!.parameterName).toBe('data');
      expect(caughtError!.zodError.issues[0].code).toBe('too_small');

      expect(service.callCount).toBe(0);
    });

    it('should validate UUID parameter with ValidateParams', async () => {
      const uuid = '123e4567-e89b-12d3-a456-426614174000';
      const result = await service.fetchUserAsync(uuid, true);

      expect(result).toEqual({ id: uuid, includeDetails: true });
      expect(service.callCount).toBe(1);
      expect(service.lastArgs).toEqual([uuid, true]);
    });

    it('should throw validation error with custom parameter name for async methods', async () => {
      let caughtError: ZodValidationError | null = null;
      try {
        await service.fetchUserAsync('invalid-uuid');
      } catch (error) {
        if (error instanceof ZodValidationError) {
          caughtError = error;
        }
      }

      expect(caughtError).not.toBeNull();
      expect(caughtError!.parameterIndex).toBe(0);
      expect(caughtError!.parameterName).toBe('userId');
      expect(caughtError!.message).toContain('Parameter validation failed for parameter "userId"');

      expect(service.callCount).toBe(0);
    });
  });

  describe('Multiple Instances', () => {
    class TestService {
      public instanceId: number;
      public callCount = 0;

      constructor(instanceId: number) {
        this.instanceId = instanceId;
      }

      @ValidateParam(0, z.string())
      processData(data: string): string {
        this.callCount++;
        return `Instance ${this.instanceId}: ${data}`;
      }
    }

    it('should maintain separate validation per instance', () => {
      const service1 = new TestService(1);
      const service2 = new TestService(2);

      const result1 = service1.processData('test1');
      const result2 = service2.processData('test2');

      expect(result1).toBe('Instance 1: test1');
      expect(result2).toBe('Instance 2: test2');
      expect(service1.callCount).toBe(1);
      expect(service2.callCount).toBe(1);

      // Both should still validate independently
      expect(() => service1.processData(123 as any)).toThrow(ZodValidationError);
      expect(() => service2.processData(456 as any)).toThrow(ZodValidationError);

      // Call counts should remain the same
      expect(service1.callCount).toBe(1);
      expect(service2.callCount).toBe(1);
    });
  });

  describe('Edge Cases', () => {
    class EdgeCaseService {
      @ValidateParams([z.string().optional()])
      processUndefined(value?: string): string {
        return value || 'default';
      }

      @ValidateParams([z.string().nullable()])
      processNull(value: string | null): string {
        return value || 'null';
      }

      @ValidateParams([z.string(), undefined, z.number().optional()])
      skipMiddleValidation(first: string, middle: any, third?: number): string {
        return `${first}-${middle}-${third || 0}`;
      }
    }

    let service: EdgeCaseService;

    beforeEach(() => {
      service = new EdgeCaseService();
    });

    it('should handle undefined with optional schema', () => {
      const result = service.processUndefined(undefined);
      expect(result).toBe('default');
    });

    it('should handle null with nullable schema', () => {
      const result = service.processNull(null);
      expect(result).toBe('null');
    });

    it('should validate string with nullable schema', () => {
      const result = service.processNull('test');
      expect(result).toBe('test');
    });

    it('should skip validation for undefined schema entries', () => {
      const result = service.skipMiddleValidation('hello', { any: 'object' }, 42);
      expect(result).toBe('hello-[object Object]-42');

      // First parameter should still be validated
      expect(() => service.skipMiddleValidation(123 as any, 'anything', 42)).toThrow(ZodValidationError);

      // Middle parameter should not be validated (can be anything)
      const result2 = service.skipMiddleValidation('hello', 'anything');
      expect(result2).toBe('hello-anything-0');
    });
  });
});