import { describe, expect, it } from 'vitest';
import {
  validateJsonFields,
  parseAndValidateJson,
  preprocessArrayFields,
  computeValidationSuppressPaths,
} from './validators';

describe('validateJsonFields', () => {
  it('should return empty errors for valid JSON object', () => {
    const fields = [{ name: 'metadata', type: 'object' }];
    const values = { metadata: '{"key": "value"}' };

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({});
  });

  it('should error on JSON array (not object)', () => {
    const fields = [{ name: 'metadata', type: 'object' }];
    const values = { metadata: '[1, 2, 3]' };

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({
      metadata: 'Must be a JSON object (not array or primitive)',
    });
  });

  it('should error on JSON primitive', () => {
    const fields = [{ name: 'metadata', type: 'object' }];
    const values = { metadata: '"string"' };

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({
      metadata: 'Must be a JSON object (not array or primitive)',
    });
  });

  it('should error on null JSON value', () => {
    const fields = [{ name: 'metadata', type: 'object' }];
    const values = { metadata: 'null' };

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({
      metadata: 'Must be a JSON object (not array or primitive)',
    });
  });

  it('should error on invalid JSON syntax', () => {
    const fields = [{ name: 'metadata', type: 'object' }];
    const values = { metadata: '{invalid json}' };

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({
      metadata: 'Invalid JSON format',
    });
  });

  it('should skip validation for empty string', () => {
    const fields = [{ name: 'metadata', type: 'object' }];
    const values = { metadata: '' };

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({});
  });

  it('should skip validation for non-object type fields', () => {
    const fields = [{ name: 'name', type: 'string' }];
    const values = { name: 'invalid json {' };

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({});
  });

  it('should skip validation for fields with itemFields', () => {
    const fields = [{ name: 'items', type: 'object', itemFields: [{ name: 'id' }] }];
    const values = { items: '[1, 2, 3]' }; // Would be error if validated

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({});
  });

  it('should validate collapsed groups', () => {
    const fields = [{ name: 'user.name' }];
    const collapsedGroups = [{ path: 'user', label: 'User' }];
    const values = { user: '{invalid}' };

    const errors = validateJsonFields(values, fields, collapsedGroups);

    expect(errors).toEqual({
      user: 'Invalid JSON format',
    });
  });

  it('should validate multiple fields', () => {
    const fields = [
      { name: 'data1', type: 'object' },
      { name: 'data2', type: 'object' },
    ];
    const values = { data1: '[1,2]', data2: '{invalid}' };

    const errors = validateJsonFields(values, fields, []);

    expect(errors).toEqual({
      data1: 'Must be a JSON object (not array or primitive)',
      data2: 'Invalid JSON format',
    });
  });
});

describe('parseAndValidateJson', () => {
  it('should parse valid JSON object', () => {
    const result = parseAndValidateJson('{"name": "Alice", "age": 30}');

    expect(result).toEqual({
      success: true,
      data: { name: 'Alice', age: 30 },
    });
  });

  it('should reject JSON array', () => {
    const result = parseAndValidateJson('[1, 2, 3]');

    expect(result).toEqual({
      success: false,
      error: 'Must be a JSON object',
    });
  });

  it('should reject JSON primitive', () => {
    const result = parseAndValidateJson('"string"');

    expect(result).toEqual({
      success: false,
      error: 'Must be a JSON object',
    });
  });

  it('should reject JSON number', () => {
    const result = parseAndValidateJson('42');

    expect(result).toEqual({
      success: false,
      error: 'Must be a JSON object',
    });
  });

  it('should reject JSON null', () => {
    const result = parseAndValidateJson('null');

    expect(result).toEqual({
      success: false,
      error: 'Must be a JSON object',
    });
  });

  it('should reject invalid JSON syntax', () => {
    const result = parseAndValidateJson('{invalid json}');

    expect(result).toEqual({
      success: false,
      error: 'Invalid JSON format',
    });
  });

  it('should handle empty object', () => {
    const result = parseAndValidateJson('{}');

    expect(result).toEqual({
      success: true,
      data: {},
    });
  });

  it('should handle nested objects', () => {
    const result = parseAndValidateJson('{"user": {"name": "Bob"}}');

    expect(result).toEqual({
      success: true,
      data: { user: { name: 'Bob' } },
    });
  });
});

describe('preprocessArrayFields', () => {
  it('should keep string[] array as-is for isArray fields (TagsInput)', () => {
    const fields = [{ name: 'tags', isArray: true }];
    const values = { tags: ['a', 'b', 'c'] };

    const result = preprocessArrayFields(values, fields);

    expect(result.tags).toEqual(['a', 'b', 'c']);
  });

  it('should convert non-array value to empty array for isArray fields', () => {
    const fields = [{ name: 'tags', isArray: true }];
    const values = { tags: 'not-an-array' };

    const result = preprocessArrayFields(values, fields);

    expect(result.tags).toEqual([]);
  });

  it('should convert undefined to empty array for isArray fields', () => {
    const fields = [{ name: 'tags', isArray: true }];
    const values = { tags: undefined };

    const result = preprocessArrayFields(values, fields);

    expect(result.tags).toEqual([]);
  });

  it('should handle empty comma-separated string', () => {
    const fields = [{ name: 'tags', isArray: true }];
    const values = { tags: '' };

    const result = preprocessArrayFields(values, fields);

    expect(result.tags).toEqual([]);
  });

  it('should handle null for isArray fields', () => {
    const fields = [{ name: 'tags', isArray: true }];
    const values = { tags: null };

    const result = preprocessArrayFields(values, fields);

    expect(result.tags).toEqual([]);
  });

  it('should skip array ref fields', () => {
    const fields = [{ name: 'tags', isArray: true, ref: { type: 'resource_id', resource: 'tag' } }];
    const values = { tags: ['a', 'b', 'c'] }; // Already array

    const result = preprocessArrayFields(values, fields);

    expect(result.tags).toEqual(['a', 'b', 'c']); // Not converted
  });

  it('should skip fields with itemFields', () => {
    const fields = [{ name: 'items', isArray: true, itemFields: [{ name: 'id' }] }];
    const values = { items: [{ id: 1 }, { id: 2 }] }; // Already array of objects

    const result = preprocessArrayFields(values, fields);

    expect(result.items).toEqual([{ id: 1 }, { id: 2 }]); // Not converted
  });

  it('should process nested array strings in itemFields', () => {
    const fields = [
      {
        name: 'items',
        itemFields: [{ name: 'id' }, { name: 'tags', isArray: true }],
      },
    ];
    const values = {
      items: [
        { id: 1, tags: ['a', 'b'] },
        { id: 2, tags: ['c', 'd', 'e'] },
      ],
    };

    const result = preprocessArrayFields(values, fields);

    expect(result.items).toEqual([
      { id: 1, tags: ['a', 'b'] },
      { id: 2, tags: ['c', 'd', 'e'] },
    ]);
  });

  it('should convert non-array nested sub-fields to empty array', () => {
    const fields = [
      {
        name: 'items',
        itemFields: [{ name: 'id' }, { name: 'tags', isArray: true }],
      },
    ];
    const values = {
      items: [
        { id: 1, tags: 'not-array' },
        { id: 2, tags: null },
      ],
    };

    const result = preprocessArrayFields(values, fields);

    expect(result.items).toEqual([
      { id: 1, tags: [] },
      { id: 2, tags: [] },
    ]);
  });

  it('should handle non-array values for isArray fields', () => {
    const fields = [{ name: 'tags', isArray: true }];
    const values = { tags: null };

    const result = preprocessArrayFields(values, fields);

    expect(result.tags).toEqual([]); // Converted to empty array
  });

  it('should handle multiple fields', () => {
    const fields = [
      { name: 'tags', isArray: true },
      { name: 'categories', isArray: true },
      { name: 'name', type: 'string' },
    ];
    const values = {
      tags: ['a', 'b'],
      categories: ['x', 'y', 'z'],
      name: 'Test',
    };

    const result = preprocessArrayFields(values, fields);

    expect(result).toEqual({
      tags: ['a', 'b'],
      categories: ['x', 'y', 'z'],
      name: 'Test',
    });
  });
});

// ============================================================================
// computeValidationSuppressPaths
// ============================================================================

describe('computeValidationSuppressPaths', () => {
  it('should include plain object fields in suppressPaths', () => {
    const fields = [{ name: 'metadata', type: 'object' as const }];
    const { suppressPaths } = computeValidationSuppressPaths(fields, []);
    expect(suppressPaths.has('metadata')).toBe(true);
  });

  it('should NOT include object fields with itemFields in suppressPaths', () => {
    const fields = [
      {
        name: 'items',
        type: 'object' as const,
        itemFields: [{ name: 'id', label: 'ID' }],
      },
    ];
    const { suppressPaths } = computeValidationSuppressPaths(fields, []);
    expect(suppressPaths.has('items')).toBe(false);
  });

  it('should include binary fields in suppressPaths', () => {
    const fields = [{ name: 'avatar', type: 'binary' as const }];
    const { suppressPaths } = computeValidationSuppressPaths(fields, []);
    expect(suppressPaths.has('avatar')).toBe(true);
  });

  it('should NOT include simple isArray fields in suppressPaths (TagsInput uses string[])', () => {
    const fields = [{ name: 'tags', isArray: true }];
    const { suppressPaths } = computeValidationSuppressPaths(fields, []);
    expect(suppressPaths.has('tags')).toBe(false);
  });

  it('should NOT include isArray fields with itemFields in suppressPaths', () => {
    const fields = [
      {
        name: 'items',
        isArray: true,
        itemFields: [{ name: 'id', label: 'ID' }],
      },
    ];
    const { suppressPaths } = computeValidationSuppressPaths(fields, []);
    expect(suppressPaths.has('items')).toBe(false);
  });

  it('should NOT include array ref fields in suppressPaths', () => {
    const fields = [
      {
        name: 'skill_ids',
        isArray: true,
        ref: { resource: 'skill', type: 'resource_id' as const },
      },
    ];
    const { suppressPaths } = computeValidationSuppressPaths(fields, []);
    expect(suppressPaths.has('skill_ids')).toBe(false);
  });

  it('should include collapsed group paths in suppressPaths', () => {
    const fields: any[] = [];
    const collapsedGroups = [
      { path: 'nested.obj', label: 'Nested' },
      { path: 'deep.path', label: 'Deep' },
    ];
    const { suppressPaths } = computeValidationSuppressPaths(fields, collapsedGroups);
    expect(suppressPaths.has('nested.obj')).toBe(true);
    expect(suppressPaths.has('deep.path')).toBe(true);
  });

  it('should collect isArray sub-fields as nestedArraySubFields', () => {
    const fields = [
      {
        name: 'equipments',
        itemFields: [
          { name: 'name', label: 'Name', type: 'string' as const },
          { name: 'effects', label: 'Effects', isArray: true },
        ],
      },
    ];
    const { nestedArraySubFields } = computeValidationSuppressPaths(fields, []);
    expect(nestedArraySubFields).toEqual([{ parent: 'equipments', sub: 'effects' }]);
  });

  it('should collect binary sub-fields as nestedArraySubFields', () => {
    const fields = [
      {
        name: 'documents',
        itemFields: [
          { name: 'title', label: 'Title', type: 'string' as const },
          { name: 'attachment', label: 'Attachment', type: 'binary' as const },
        ],
      },
    ];
    const { nestedArraySubFields } = computeValidationSuppressPaths(fields, []);
    expect(nestedArraySubFields).toEqual([{ parent: 'documents', sub: 'attachment' }]);
  });

  it('should collect both isArray and binary sub-fields', () => {
    const fields = [
      {
        name: 'items',
        itemFields: [
          { name: 'name', label: 'Name', type: 'string' as const },
          { name: 'tags', label: 'Tags', isArray: true },
          { name: 'file', label: 'File', type: 'binary' as const },
        ],
      },
    ];
    const { nestedArraySubFields } = computeValidationSuppressPaths(fields, []);
    expect(nestedArraySubFields).toEqual([
      { parent: 'items', sub: 'tags' },
      { parent: 'items', sub: 'file' },
    ]);
  });

  it('should return empty sets for no fields and no groups', () => {
    const { suppressPaths, nestedArraySubFields } = computeValidationSuppressPaths([], []);
    expect(suppressPaths.size).toBe(0);
    expect(nestedArraySubFields).toEqual([]);
  });

  it('should skip fields without itemFields for nestedArraySubFields', () => {
    const fields = [
      { name: 'tags', isArray: true },
      { name: 'avatar', type: 'binary' as const },
    ];
    const { nestedArraySubFields } = computeValidationSuppressPaths(fields, []);
    expect(nestedArraySubFields).toEqual([]);
  });

  it('should handle comprehensive mixed field scenario', () => {
    const fields = [
      { name: 'metadata', type: 'object' as const },
      { name: 'avatar', type: 'binary' as const },
      { name: 'tags', isArray: true },
      {
        name: 'skill_ids',
        isArray: true,
        ref: { resource: 'skill', type: 'resource_id' as const },
      },
      {
        name: 'equipments',
        itemFields: [
          { name: 'name', label: 'Name', type: 'string' as const },
          { name: 'effects', label: 'Effects', isArray: true },
          { name: 'icon', label: 'Icon', type: 'binary' as const },
        ],
      },
      { name: 'title', type: 'string' as const },
    ];
    const collapsedGroups = [{ path: 'nested.deep', label: 'Deep Nested' }];

    const { suppressPaths, nestedArraySubFields } = computeValidationSuppressPaths(
      fields,
      collapsedGroups,
    );

    // Should be suppressed
    expect(suppressPaths.has('metadata')).toBe(true);
    expect(suppressPaths.has('avatar')).toBe(true);
    expect(suppressPaths.has('nested.deep')).toBe(true);

    // Should NOT be suppressed (TagsInput uses string[], no longer needs suppression)
    expect(suppressPaths.has('tags')).toBe(false);
    expect(suppressPaths.has('skill_ids')).toBe(false);
    expect(suppressPaths.has('equipments')).toBe(false);
    expect(suppressPaths.has('title')).toBe(false);

    // Nested sub-fields
    expect(nestedArraySubFields).toEqual([
      { parent: 'equipments', sub: 'effects' },
      { parent: 'equipments', sub: 'icon' },
    ]);
  });
});
