import { CubeValidator, functionFieldsPatterns } from '../../src/compiler/CubeValidator';
import {
  CubeRefreshKey,
  CubeSymbols,
  PreAggregationDefinitionOriginalSql
} from '../../src/compiler/CubeSymbols';
import { ErrorReporter } from '../../src/compiler/ErrorReporter';

describe('Cube Validation', () => {
  class ConsoleErrorReporter extends ErrorReporter {
    public error(message: any, _e: any) {
      console.log(message);
    }
  }

  it('transpiledFieldsPatterns', async () => {
    const transpiledFieldsPatterns = functionFieldsPatterns()
      .filter((p) => p.indexOf('extends') < 0 && p.indexOf('allDefinitions') < 0)
      .map((p) => {
        p = p.replace(/\./g, '\\.').replace(/\*/g, '[_a-zA-Z][_a-zA-Z0-9]*');
        return RegExp(`^${p}$`);
      });
    transpiledFieldsPatterns.push(/^contextMembers$/);
    transpiledFieldsPatterns.push(/\.sql$/);

    console.log('CubePropContextTranspiler.transpiledFieldsPatterns =', transpiledFieldsPatterns);
  });

  it('cube all ways - correct', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => 'SELECT * FROM public.Users',
      public: true,
      measures: {
        min: {
          public: true,
          sql: () => 'amount',
          type: 'min'
        },
        max: {
          // old way
          shown: true,
          sql: () => 'amount',
          type: 'max'
        },
      },
      dimensions: {
        createdAt: {
          public: true,
          sql: () => 'created_at',
          type: 'time'
        },
        pkey: {
          // old way
          shown: true,
          sql: () => 'id',
          type: 'number',
          primaryKey: true
        },
      },
      segments: {
        firstSegment: {
          public: false,
          sql: () => 'test',
        },
        secondSegment: {
          shown: false,
          sql: () => 'test',
        }
      },
      fileName: 'fileName',
    };

    const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
    expect(validationResult.error).toBeFalsy();
  });

  it('cube defined with sql - correct', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => 'SELECT * FROM public.Users',
      fileName: 'fileName',
    };

    const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());

    expect(validationResult.error).toBeFalsy();
  });

  it('cube defined with sqlTable - correct', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sqlTable: () => 'public.Users',
      fileName: 'fileName',
    };

    const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
    expect(validationResult.error).toBeFalsy();
  });

  it('cube defined with sql and sqlTable - fail', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => 'SELECT * FROM public.Users',
      sqlTable: () => 'public.Users',
      fileName: 'fileName',
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('You must use either sql or sqlTable within a model, but not both');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('view defined by includes - correct', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      // it's a hidden field which we use internally
      isView: true,
      fileName: 'fileName',
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
      }
    } as any);

    expect(validationResult.error).toBeFalsy();
  });

  it('view with incorrect included member with alias', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      // it's a hidden field which we use internally
      isView: true,
      fileName: 'fileName',
      cubes: [
        {
          joinPath: () => '',
          prefix: false,
          includes: [
            'member-by-name',
            {
              name: 'member-by-alias',
              alias: 'incorrect Alias'
            }
          ]
        }
      ]
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('view with overridden included members properties', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      // it's a hidden field which we use internally
      isView: true,
      fileName: 'fileName',
      cubes: [
        {
          joinPath: () => '',
          prefix: false,
          includes: [
            'member_by_name',
            {
              name: 'member_by_alias',
              alias: 'correct_alias'
            },
            {
              name: 'member_by_alias_with_overrides',
              title: 'Overridden title',
              description: 'Overridden description',
              format: 'percent',
              meta: {
                f1: 'Overridden 1',
                f2: 'Overridden 2',
              },
            }
          ]
        }
      ]
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
      }
    } as any);

    expect(validationResult.error).toBeFalsy();
  });

  describe('view default value filters', () => {
    const silentReporter = new ConsoleErrorReporter();

    it('view with default value filter - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'orders_view',
        isView: true,
        fileName: 'fileName',
        defaultFilters: [
          {
            member: () => 'currency',
            operator: 'equals',
            values: () => ['USD'],
          },
        ],
      };

      const validationResult = cubeValidator.validate(cube, silentReporter);

      expect(validationResult.error).toBeFalsy();
    });

    it('view with default value filter and unless - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'orders_view',
        isView: true,
        fileName: 'fileName',
        defaultFilters: [
          {
            member: () => 'currency',
            operator: 'equals',
            values: () => ['USD'],
            unless: () => ['currency'],
          },
        ],
      };

      const validationResult = cubeValidator.validate(cube, silentReporter);

      expect(validationResult.error).toBeFalsy();
    });

    it('view filter with set operator does not require values - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'orders_view',
        isView: true,
        fileName: 'fileName',
        defaultFilters: [
          {
            member: () => 'currency',
            operator: 'set',
          },
        ],
      };

      const validationResult = cubeValidator.validate(cube, silentReporter);

      expect(validationResult.error).toBeFalsy();
    });

    it('view filter with missing required values - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'orders_view',
        isView: true,
        fileName: 'fileName',
        defaultFilters: [
          {
            member: () => 'currency',
            operator: 'equals',
          },
        ],
      };

      const validationResult = cubeValidator.validate(cube, silentReporter);

      expect(validationResult.error).toBeTruthy();
    });

    it('view filter with missing member - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'orders_view',
        isView: true,
        fileName: 'fileName',
        defaultFilters: [
          {
            operator: 'equals',
            values: () => ['USD'],
          },
        ],
      };

      const validationResult = cubeValidator.validate(cube, silentReporter);

      expect(validationResult.error).toBeTruthy();
    });

    it('view filter with invalid operator - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'orders_view',
        isView: true,
        fileName: 'fileName',
        defaultFilters: [
          {
            member: () => 'currency',
            operator: 'someInvalidOperator',
            values: () => ['USD'],
          },
        ],
      };

      const validationResult = cubeValidator.validate(cube, silentReporter);

      expect(validationResult.error).toBeTruthy();
    });

    it('regular cube with default value filter - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'orders',
        sql: () => 'SELECT * FROM orders',
        fileName: 'fileName',
        defaultFilters: [
          {
            member: () => 'currency',
            operator: 'equals',
            values: () => ['USD'],
          },
        ],
      };

      const validationResult = cubeValidator.validate(cube, silentReporter);

      expect(validationResult.error).toBeTruthy();
    });
  });

  it('refreshKey alternatives', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      refreshKey: {
        every: '12h',
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('(refreshKey.every = 12h)');
        expect(message).toContain('does not match regexp');
        expect(message).toContain('CronParser');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('refreshKey timezones', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      refreshKey: {
        every: '0 * * * *',
        timezone: 'AAA'
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('unknown timezone');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  describe('Measures', () => {
    it('measures alternatives', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          number: {
            type: 'suma'
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('must be one of [count, number,');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('2 timeShifts, 1 without timeDimension', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          measure_with_time_shift: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            timeShift: [
              {
                timeDimension: () => '',
                interval: '1 day',
                type: 'prior',
              },
              {
                interval: '1 day',
                type: 'prior',
              }
            ]
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('(measures.measure_with_time_shift.timeShift[1].timeDimension) is required');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('3 timeShifts', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          measure_with_time_shift: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            timeShift: [
              {
                timeDimension: () => '',
                interval: '1 day',
                type: 'prior',
              },
              {
                timeDimension: () => '',
                interval: '1 year',
                type: 'next',
              },
              {
                timeDimension: () => '',
                interval: '1 week',
                type: 'prior',
              }
            ]
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
        }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('1 timeShift without timeDimension', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          measure_with_time_shift: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            timeShift: [
              {
                interval: '1 day',
                type: 'prior',
              }
            ]
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
        }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('multi-stage filter — full directive accepted', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          measure_with_filter: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            filter: {
              mode: 'relative',
              exclude: () => [],
              include: [
                { member: 'cube.dim', operator: 'equals', values: ['x'] },
                { or: [
                  { member: 'cube.dim', operator: 'equals', values: ['y'] },
                  { member: 'cube.other', operator: 'set' },
                ] },
              ],
            }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
        }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('multi-stage filter — partial directives accepted', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          only_exclude: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            filter: { mode: 'fixed', exclude: () => [] }
          },
          only_include: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            filter: {
              include: [{ member: 'cube.dim', operator: 'equals', values: ['x'] }]
            }
          },
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
        }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('multi-stage filter — invalid mode rejected', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          bad_mode: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            filter: { mode: 'RELATIVE' }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('measures.bad_mode.filter.mode');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('multi-stage filter — include item without member/operator rejected', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          bad_include: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            filter: { include: [{}] }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('measures.bad_include.filter.include[0]');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('multi-stage filter — include with bad operator rejected', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          bad_op: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            filter: {
              include: [
                { member: 'cube.dim', operator: 'totallyNotAnOperator', values: ['x'] }
              ]
            }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('measures.bad_op.filter.include[0]');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('multi-stage filter — exclude and keepOnly are mutually exclusive', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          both_set: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            filter: {
              exclude: () => [],
              keepOnly: () => [],
            }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('exclude');
          expect(message).toContain('keepOnly');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('multi-stage filter — exclude must be a function', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          bad_exclude: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            filter: { exclude: ['some_dim'] }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('measures.bad_exclude.filter.exclude');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('multi-stage filter — accepted on multi-stage dimension', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        dimensions: {
          dim_with_filter: {
            multiStage: true,
            type: 'string',
            sql: () => '',
            filter: {
              mode: 'fixed',
              keepOnly: () => [],
              include: [{ member: 'cube.dim', operator: 'equals', values: ['x'] }],
            }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
        }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('multi-stage grain — full directive accepted', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          measure_with_grain: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            grain: {
              exclude: () => [],
              include: () => [],
            }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
        }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('multi-stage grain — partial directives accepted', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          only_exclude: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            grain: { exclude: () => [] }
          },
          only_keep_only: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            grain: { keepOnly: () => [] }
          },
          only_include: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            grain: { include: () => [] }
          },
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
        }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('multi-stage grain — exclude and keepOnly are mutually exclusive', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          both_set: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            grain: {
              exclude: () => [],
              keepOnly: () => [],
            }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('exclude');
          expect(message).toContain('keepOnly');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('multi-stage grain — exclude must be a function', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          bad_exclude: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            grain: { exclude: ['some_dim'] }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('measures.bad_exclude.grain.exclude');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('multi-stage grain — include must be a function', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        measures: {
          bad_include: {
            multiStage: true,
            type: 'sum',
            sql: () => '',
            grain: { include: ['some_dim'] }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('measures.bad_include.grain.include');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('multi-stage grain — rejected on multi-stage dimension', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => '',
        fileName: 'fileName',
        dimensions: {
          dim_with_grain: {
            multiStage: true,
            type: 'string',
            sql: () => '',
            grain: { include: () => [] }
          }
        }
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });
  });

  it('OriginalSqlSchema', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        eventsByType: {
          type: 'originalSql',
          partitionGranularity: 'day'
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('timeDimension) is required');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('RollUpJoinSchema', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        eventsByType: {
          type: 'rollupJoin',
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('granularity) is required');
        expect(message).toContain('rollups) is required');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('RollUpJoinSchema timeDimension', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        eventsByType: {
          type: 'rollupJoin',
          measures: () => '',
          dimensions: () => '',
          partitionGranularity: 'month',
          timeDimension: () => 'td',
          external: true,
          rollups: () => 0,
          refreshKey: {
            every: '10 minutes',
            updateWindow: '250 day',
            incremental: true
          },
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('granularity) is required');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('RollUpJoinSchema scheduledRefresh', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      fileName: 'filename',
      name: 'name',
      sql: () => '',
      preAggregations: {
        eventsByType: {
          type: 'rollupJoin',
          granularity: 'month',
          scheduledRefresh: true,
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('(preAggregations.eventsByType.scheduledRefresh = true) must be [false]');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('indexes alternatives', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        eventsByType: {
          type: 'originalSql',
          originalSql: () => '',
          indexes: {
            number: {
            }
          }
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('number.sql) is required');
        expect(message).toContain('number.columns) is required');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('preAggregations custom granularities', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        distinct: {
          type: 'rollup',
          measureReferences: () => '',
          dimensionReferences: () => '',
          partitionGranularity: 'month',
          granularity: 'custom_granularity_name',
          timeDimensionReference: () => '',
          external: true,
          refreshKey: {
            every: '10 minutes',
            updateWindow: '250 day',
            incremental: true
          },
        },
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (_message: any, _e: any) => {
        // this callback should not be invoked
        expect(true).toBeFalsy();
      }
    } as any);

    expect(validationResult.error).toBeFalsy();
  });

  it('preAggregations type unknown', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        eventsByType: {
          type: 'AAA',
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('must be');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('preAggregations deprecated fields', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        eventsByType: {
          rollupReferences: () => '',
          measures: () => '',
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        expect(message).toContain('are deprecated, please, use');
      }
    } as any);

    expect(validationResult.error).toBeTruthy();
  });

  it('No errors', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        eventsByType: {
          type: 'rollup',
          partitionGranularity: 'day',
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        // this callback should not be invoked
        expect(true).toBeFalsy();
      }
    } as any);

    expect(validationResult.error).toBeFalsy();
  });

  it('Partition with multi time dimensions', async () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());
    const cube = {
      name: 'name',
      sql: () => '',
      fileName: 'fileName',
      preAggregations: {
        eventsByType: {
          type: 'rollup',
          timeDimensions: [
            {
              dimension: () => 'field1',
              granularity: 'day'
            },
            {
              dimension: () => 'field2',
              granularity: 'day'
            }
          ],
          partitionGranularity: 'day',
        }
      }
    };

    const validationResult = cubeValidator.validate(cube, {
      error: (message: any, _e: any) => {
        console.log(message);
        // this callback should not be invoked
        expect(true).toBeFalsy();
      }
    } as any);

    expect(validationResult.error).toBeFalsy();
  });

  test('cube - aliases test', async () => {
    const cubeA = {
      name: 'CubeA',
      sql_table: () => 'public.Users',
      public: true,
      refresh_key: {
        sql: () => 'SELECT MAX(created_at) FROM orders',
      },
      measures: {
        id: {
          sql: () => 'id',
          type: 'count',
          drill_members: () => ['pkey', 'createdAt'],
          rolling_window: {
            trailing: '1 month',
          }
        },
      },
      dimensions: {
        pkey: {
          shown: true,
          sql: () => 'id',
          type: 'number',
          subQuery: true,
          primary_key: true,
          propagate_filters_to_sub_query: true
        },
        createdAt: {
          sql: () => 'created',
          type: 'time',
        },
      },
      pre_aggregations: {
        main: {
          type: 'originalSql',
          time_dimension: () => 'createdAt',
          partition_granularity: 'day',
          refreshKey: {
            sql: () => 'SELECT MAX(created_at) FROM orders',
          } satisfies CubeRefreshKey,
        } satisfies PreAggregationDefinitionOriginalSql
      },
      data_source: 'default',
      rewrite_queries: true,
      sql_alias: 'myalias',
      fileName: 'fileName',
    };

    const cubeSymbols = new CubeSymbols();
    cubeSymbols.compile([cubeA], {
      // @ts-ignore
      inContext: () => false,
      error: (message, _e) => {
        console.log(message);
      }
    });

    const cubeValidator = new CubeValidator(cubeSymbols);
    const validationResult = cubeValidator.validate(cubeSymbols.getCubeDefinition('CubeA'), {
      inContext: () => false,
      error: (message: any, _e: any) => {
        console.log(message);
      }
    } as any);

    expect(validationResult.error).toBeFalsy();
  });

  describe('Custom dimension granularities: ', () => {
    const newCube = (granularities) => ({
      name: 'Orders',
      fileName: 'fileName',
      sql: () => 'select * from tbl',
      public: true,
      dimensions: {
        createdAt: {
          public: true,
          sql: () => 'created_at',
          type: 'time',
          granularities
        },
        status: {
          type: 'string',
          sql: () => 'status',
        }
      },
      measures: {
        count: {
          sql: () => 'count',
          type: 'count'
        }
      }
    });

    it('no granularity interval', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = newCube({
        half_year: {}
      });

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, e: any) => {
          console.log(message);
          expect(message).toContain('(dimensions.createdAt.granularities.half_year.interval) is required');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('granularity with aligned interval', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      {
        const cube = newCube({
          half_year: {
            interval: '10 years' // useless, but still valid
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '6 months',
            title: 'Half year intervals'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '1 day'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '6 hours'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '15 minutes'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '30 seconds'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }
    });

    it('granularity with aligned interval + offset', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      {
        const cube = newCube({
          half_year: {
            interval: '10 years', // useless, but still valid
            offset: '2 months 3 weeks 4 days',
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '6 months',
            offset: '4 weeks 5 days 6 hours',
            title: 'Half year intervals title'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '1 day',
            offset: '5 days 6 hours 7 minutes',
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '6 hours',
            offset: '5 days 6 hours 7 minutes 8 seconds',
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '15 minutes',
            offset: '1 hours 7 minutes 8 seconds',
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '30 seconds',
            offset: '8 seconds',
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }
    });

    it('granularity with unaligned interval', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());

      {
        const cube = newCube({
          half_year: {
            interval: '5 months',
          }
        });

        const validationResult = cubeValidator.validate(cube, {
          error: (message: any, _e: any) => {
            console.log(message);
            expect(message).toContain('"dimensions.createdAt" does not match any of the allowed types');
            expect(message).toContain('Arbitrary intervals cannot be used without origin point specified');
          }
        } as any);

        expect(validationResult.error).toBeTruthy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '3 quarters',
          }
        });

        const validationResult = cubeValidator.validate(cube, {
          error: (message: any, _e: any) => {
            console.log(message);
            expect(message).toContain('"dimensions.createdAt" does not match any of the allowed types');
            expect(message).toContain('Arbitrary intervals cannot be used without origin point specified');
          }
        } as any);

        expect(validationResult.error).toBeTruthy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '3 weeks',
          }
        });

        const validationResult = cubeValidator.validate(cube, {
          error: (message: any, _e: any) => {
            console.log(message);
            expect(message).toContain('"dimensions.createdAt" does not match any of the allowed types');
            expect(message).toContain('Arbitrary intervals cannot be used without origin point specified');
          }
        } as any);

        expect(validationResult.error).toBeTruthy();
      }

      // Offset doesn't matter in this case
      {
        const cube = newCube({
          half_year: {
            interval: '15 days',
            offset: '1 hours 7 minutes 8 seconds',
            title: 'Just title'
          }
        });

        const validationResult = cubeValidator.validate(cube, {
          error: (message: any, _e: any) => {
            console.log(message);
            expect(message).toContain('"dimensions.createdAt" does not match any of the allowed types');
          }
        } as any);

        expect(validationResult.error).toBeTruthy();
      }
    });

    it('granularity with invalid interval', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = newCube({
        half_year: {
          interval: 'invalid',
        }
      });

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('"dimensions.createdAt" does not match any of the allowed types');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('granularity with origin + invalid interval', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = newCube({
        half_year: {
          origin: '2024',
          interval: 'invalid',
        }
      });

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('"dimensions.createdAt" does not match any of the allowed types');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('granularity with invalid origin + interval', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = newCube({
        half_year: {
          origin: 'invalid',
          interval: '3 months',
        }
      });

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('"dimensions.createdAt" does not match any of the allowed types');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });

    it('granularity with origin + interval', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());

      {
        const cube = newCube({
          half_year: {
            interval: '10 years', // useless, but still valid
            origin: '2024',
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '10 months',
            origin: '2024-04',
            title: 'Someone loves number 10'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '2 quarters',
            origin: '2024-04',
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '15 day',
            origin: '2024-05-25',
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '8 hours',
            origin: '2024-09-20 10:00'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '15 minutes',
            origin: '2024-09-20 16:40'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '30 seconds',
            origin: '2024-09-20 16:40:33'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '2 months 30 seconds',
            origin: '2024-09-20T16:40:33.345'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }

      {
        const cube = newCube({
          half_year: {
            interval: '2 months 12 days 14 hours 30 seconds',
            origin: '2024-09-20T16:40:33.345Z'
          }
        });

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      }
    });
  });

  describe('Access Policy group/groups support:', () => {
    const cubeValidator = new CubeValidator(new CubeSymbols());

    it('should allow group', () => {
      const cube = {
        name: 'TestCube',
        fileName: 'test.js',
        sql: () => 'SELECT * FROM test',
        accessPolicy: [{
          group: 'admin',
          rowLevel: { allowAll: true }
        }]
      };

      const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(result.error).toBeFalsy();
    });

    it('should allow groups as array', () => {
      const cube = {
        name: 'TestCube',
        fileName: 'test.js',
        sql: () => 'SELECT * FROM test',
        accessPolicy: [{
          groups: ['admin', 'user'],
          rowLevel: { allowAll: true }
        }]
      };

      const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(result.error).toBeFalsy();
    });

    it('should allow group: "*" syntax', () => {
      const cube = {
        name: 'TestCube',
        fileName: 'test.js',
        sql: () => 'SELECT * FROM test',
        accessPolicy: [{
          group: '*',
          rowLevel: { allowAll: true }
        }]
      };

      const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(result.error).toBeFalsy();
    });

    it('should reject group and groups together', () => {
      const cube = {
        name: 'TestCube',
        fileName: 'test.js',
        sql: () => 'SELECT * FROM test',
        accessPolicy: [{
          group: 'admin',
          groups: ['user'],
          rowLevel: { allowAll: true }
        }]
      };

      const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(result.error).toBeTruthy();
    });

    it('should reject access policy without group/groups', () => {
      const cube = {
        name: 'TestCube',
        fileName: 'test.js',
        sql: () => 'SELECT * FROM test',
        accessPolicy: [{
          rowLevel: { allowAll: true }
        }]
      };

      const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(result.error).toBeTruthy();
    });
  });

  describe('Custom time format for time dimensions (strptime)', () => {
    it('time dimension with valid strptime format - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: '%Y-%m-%d'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('time dimension with complex strptime format - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: '%d/%m/%Y %H:%M:%S'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('time dimension with literal text in format - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: '%Y Year %m Month'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('time dimension with escaped percent - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: '%Y-%m-%d %%'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('time dimension with standard format - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: 'id'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('time dimension with invalid format (no specifiers) - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: 'invalid'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });

    it('time dimension with invalid specifier - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: '%Y-%K-%d'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });

    it('time dimension with incomplete specifier at end - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: '%Y-%m-%'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });

    it('time dimension with only escaped percent - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            format: '%%'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });

    it('non-time dimension with strptime format string - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          status: {
            sql: () => 'status',
            type: 'string',
            format: '%Y-%m-%d'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });
  });

  describe('Custom numeric format for measures (d3-format)', () => {
    it('measures with valid d3-format and standard formats - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        measures: {
          // .2f - fixed-point with 2 decimal places
          amount: {
            sql: () => 'amount',
            type: 'sum',
            format: '.2f'
          },
          // ,.0f - thousands separator, no decimals
          revenueGrouped: {
            sql: () => 'revenue',
            type: 'sum',
            format: ',.0f'
          },
          // $,.2f - currency symbol with grouping
          revenueCurrency: {
            sql: () => 'revenue',
            type: 'sum',
            format: '$,.2f'
          },
          // .0% - percentage format
          conversionRate: {
            sql: () => 'conversion_rate',
            type: 'avg',
            format: '.0%'
          },
          // .2s - SI prefix notation (e.g., 1.2k, 3.4M)
          bytes: {
            sql: () => 'bytes',
            type: 'sum',
            format: '.2s'
          },
          // +.2f - always show sign
          change: {
            sql: () => 'change',
            type: 'sum',
            format: '+.2f'
          },
          // 010d - zero-padded integer
          orderId: {
            type: 'count',
            format: '010d'
          },
          // .2~f - trim trailing zeros
          trimmed: {
            sql: () => 'amount',
            type: 'sum',
            format: '.2~f'
          },
          // Standard formats still work
          ratioPercent: {
            sql: () => 'ratio',
            type: 'avg',
            format: 'percent'
          },
          revenueCurrencyStandard: {
            sql: () => 'revenue',
            type: 'sum',
            format: 'currency'
          },
          countNumber: {
            type: 'count',
            format: 'number'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('measure with invalid format (unknown type character) - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        measures: {
          amount: {
            sql: () => 'amount',
            type: 'sum',
            format: '.2z' // 'z' is not a valid d3-format type
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });

    it('measure with invalid format (random string) - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        measures: {
          amount: {
            sql: () => 'amount',
            type: 'sum',
            format: 'invalid-format'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });
  });

  describe('Named numeric formats', () => {
    it('measures with valid named formats - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        measures: {
          num3: { sql: () => 'amount', type: 'sum', format: 'number_3' },
          pct1: { sql: () => 'ratio', type: 'avg', format: 'percent_1' },
          cur4: { sql: () => 'revenue', type: 'sum', format: 'currency_4' },
          dec5: { sql: () => 'amount', type: 'sum', format: 'decimal_5' },
          ab: { sql: () => 'bytes', type: 'sum', format: 'abbr' },
          ab2: { sql: () => 'bytes', type: 'sum', format: 'abbr_2' },
          acc: { sql: () => 'amount', type: 'sum', format: 'accounting' },
          acc0: { sql: () => 'amount', type: 'sum', format: 'accounting_0' },
          ident: { sql: () => 'order_id', type: 'count', format: 'id' },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('number dimensions with valid named formats - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          price: { sql: () => 'price', type: 'number', format: 'number_2' },
          discount: { sql: () => 'discount', type: 'number', format: 'percent_0' },
          total: { sql: () => 'total', type: 'number', format: 'currency_1' },
          pop: { sql: () => 'pop', type: 'number', format: 'abbr_3' },
          ab: { sql: () => 'ab', type: 'number', format: 'abbr' },
          bal: { sql: () => 'bal', type: 'number', format: 'accounting' },
          dec: { sql: () => 'dec', type: 'number', format: 'decimal_4' },
          dec2: { sql: () => 'dec2', type: 'number', format: 'decimal' },
          ident: { sql: () => 'ident', type: 'number', format: 'id' },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('string dimension with named numeric format - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          status: { sql: () => 'status', type: 'string', format: 'abbr_2' },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });
  });

  describe('Custom numeric format for number dimensions (d3-format)', () => {
    it('number dimensions with valid d3-format and standard formats - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          // d3-format specifiers
          price: {
            sql: () => 'price',
            type: 'number',
            format: '.2f'
          },
          quantity: {
            sql: () => 'quantity',
            type: 'number',
            format: ',.0f'
          },
          unitPrice: {
            sql: () => 'unit_price',
            type: 'number',
            format: '$,.2f'
          },
          // Standard dimension formats work for number type
          discount: {
            sql: () => 'discount',
            type: 'number',
            format: 'percent'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('number dimension with invalid format - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          price: {
            sql: () => 'price',
            type: 'number',
            format: 'invalid-format'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });

    it('string dimension with d3-format string - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          status: {
            sql: () => 'status',
            type: 'string',
            format: '.2f' // d3-format not allowed for string type
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeTruthy();
    });

    describe('Currency property', () => {
      it('measure with valid currency (lowercase input) - correct', async () => {
        const cubeValidator = new CubeValidator(new CubeSymbols());
        const cube = {
          name: 'name',
          sql: () => 'SELECT * FROM public.Orders',
          measures: {
            amount: {
              sql: () => 'amount',
              type: 'sum',
              currency: 'usd'
            },
          },
          fileName: 'fileName',
        };

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      });

      it('measure with invalid currency (too long) - error', async () => {
        const cubeValidator = new CubeValidator(new CubeSymbols());
        const cube = {
          name: 'name',
          sql: () => 'SELECT * FROM public.Orders',
          measures: {
            amount: {
              sql: () => 'amount',
              type: 'sum',
              currency: 'usdx'
            },
          },
          fileName: 'fileName',
        };

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeTruthy();
        expect(validationResult.error!.message).toContain(
          '"usdx" is not a valid currency code. Expected a valid 3-letter ISO 4217 code (e.g. USD, EUR)'
        );
      });

      it('measure with invalid currency (too short) - error', async () => {
        const cubeValidator = new CubeValidator(new CubeSymbols());
        const cube = {
          name: 'name',
          sql: () => 'SELECT * FROM public.Orders',
          measures: {
            amount: {
              sql: () => 'amount',
              type: 'sum',
              currency: 'us'
            },
          },
          fileName: 'fileName',
        };

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeTruthy();
        expect(validationResult.error!.message).toContain(
          '"us" is not a valid currency code. Expected a valid 3-letter ISO 4217 code (e.g. USD, EUR)'
        );
      });

      it('measure with invalid currency (numeric) - error', async () => {
        const cubeValidator = new CubeValidator(new CubeSymbols());
        const cube = {
          name: 'name',
          sql: () => 'SELECT * FROM public.Orders',
          measures: {
            amount: {
              sql: () => 'amount',
              type: 'sum',
              currency: '123'
            },
          },
          fileName: 'fileName',
        };

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeTruthy();
        expect(validationResult.error!.message).toContain(
          '"123" is not a valid currency code. Expected a valid 3-letter ISO 4217 code (e.g. USD, EUR)'
        );
      });

      it('number dimension with valid currency - correct', async () => {
        const cubeValidator = new CubeValidator(new CubeSymbols());
        const cube = {
          name: 'name',
          sql: () => 'SELECT * FROM public.Orders',
          dimensions: {
            price: {
              sql: () => 'price',
              type: 'number',
              currency: 'eur'
            },
          },
          fileName: 'fileName',
        };

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      });

      it('non-number dimension with currency - error', async () => {
        const cubeValidator = new CubeValidator(new CubeSymbols());
        const cube = {
          name: 'name',
          sql: () => 'SELECT * FROM public.Orders',
          dimensions: {
            status: {
              sql: () => 'status',
              type: 'string',
              currency: 'usd'
            },
          },
          fileName: 'fileName',
        };

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeTruthy();
        const nestedMessages = validationResult.error!.details[0]?.context?.details?.map((d: any) => d.message);
        expect(nestedMessages).toContain(
          '"currency" property can only be used with dimensions of type "number"'
        );
      });

      it('non-numeric measure with currency - error', async () => {
        const cubeValidator = new CubeValidator(new CubeSymbols());
        const cube = {
          name: 'name',
          sql: () => 'SELECT * FROM public.Orders',
          measures: {
            label: {
              sql: () => 'label',
              type: 'string',
              currency: 'usd'
            },
          },
          fileName: 'fileName',
        };

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeTruthy();
        expect(validationResult.error!.message).toEqual(
          '"currency" property can only be used with numeric measures, actual type: string'
        );
      });

      it('measure with both format and currency - correct', async () => {
        const cubeValidator = new CubeValidator(new CubeSymbols());
        const cube = {
          name: 'name',
          sql: () => 'SELECT * FROM public.Orders',
          measures: {
            amount: {
              sql: () => 'amount',
              type: 'sum',
              format: 'currency',
              currency: 'gbp'
            },
          },
          fileName: 'fileName',
        };

        const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
        expect(validationResult.error).toBeFalsy();
      });
    });

    it('dimension with valid order asc - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          status: {
            sql: () => 'status',
            type: 'string',
            order: 'asc'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('dimension with valid order desc - correct', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          createdAt: {
            sql: () => 'created_at',
            type: 'time',
            order: 'desc'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, new ConsoleErrorReporter());
      expect(validationResult.error).toBeFalsy();
    });

    it('dimension with invalid order value - error', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'name',
        sql: () => 'SELECT * FROM public.Orders',
        dimensions: {
          status: {
            sql: () => 'status',
            type: 'string',
            order: 'invalid' // should only accept 'asc' or 'desc'
          },
        },
        fileName: 'fileName',
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (message: any, _e: any) => {
          console.log(message);
          expect(message).toContain('order');
        }
      } as any);

      expect(validationResult.error).toBeTruthy();
    });
  });

  describe('View joinPath validation - unique leaf nodes', () => {
    it('view with duplicate leaf nodes - should fail', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A.D.E.X', prefix: false, includes: ['*'] },
          { joinPath: () => 'A.F.X', prefix: false, includes: ['*'] }
        ]
      };

      let errorMessage = '';
      cubeValidator.validate(cube, {
        error: (message: any) => { errorMessage = message; }
      } as any);

      expect(errorMessage).toContain('Views can\'t define multiple join paths to the same cube');
      expect(errorMessage).toContain('View \'test_view\'');
      expect(errorMessage).toContain('has multiple paths to \'X\'');
      expect(errorMessage).toContain('within root \'A\'');
      expect(errorMessage).toContain('\'A.D.E.X\'');
      expect(errorMessage).toContain('\'A.F.X\'');
      expect(errorMessage).toContain('Use extends to create a child cube');
    });

    it('view with unique leaf nodes - should pass', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A', prefix: false, includes: ['*'] },
          { joinPath: () => 'A.D', prefix: false, includes: ['*'] },
          { joinPath: () => 'A.D.E.X', prefix: false, includes: ['*'] }
        ]
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (msg: any) => { throw new Error(`Unexpected error: ${msg}`); }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('view with conflict in one root only - should report only that root', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A.B.X', prefix: false, includes: ['*'] },
          { joinPath: () => 'A.C.X', prefix: false, includes: ['*'] },
          { joinPath: () => 'D.E.X', prefix: false, includes: ['*'] }
        ]
      };

      let errorMessage = '';
      cubeValidator.validate(cube, {
        error: (msg: any) => { errorMessage = msg; }
      } as any);

      expect(errorMessage).toContain('has multiple paths to \'X\'');
      expect(errorMessage).toContain('within root \'A\'');
      expect(errorMessage).toContain('\'A.B.X\'');
      expect(errorMessage).toContain('\'A.C.X\'');
      // D.E.X should NOT be in the error message as it's a different root
      expect(errorMessage).not.toContain('\'D.E.X\'');
    });

    it('view with multiple different conflicts in different roots - should report all', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A.B.X', prefix: false, includes: ['*'] },
          { joinPath: () => 'A.D.X', prefix: false, includes: ['*'] },
          { joinPath: () => 'C.E.Y', prefix: false, includes: ['*'] },
          { joinPath: () => 'C.F.Y', prefix: false, includes: ['*'] }
        ]
      };

      const errorMessages: string[] = [];
      cubeValidator.validate(cube, {
        error: (msg: any) => { errorMessages.push(msg); }
      } as any);

      const allErrors = errorMessages.join(' ');
      expect(allErrors).toContain('has multiple paths to \'X\'');
      expect(allErrors).toContain('has multiple paths to \'Y\'');
      expect(allErrors).toContain('within root \'A\'');
      expect(allErrors).toContain('within root \'C\'');
      expect(allErrors).toContain('\'A.B.X\'');
      expect(allErrors).toContain('\'A.D.X\'');
      expect(allErrors).toContain('\'C.E.Y\'');
      expect(allErrors).toContain('\'C.F.Y\'');
    });

    it('view with single-segment paths - should pass', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A', prefix: false, includes: ['*'] },
          { joinPath: () => 'B', prefix: false, includes: ['*'] },
          { joinPath: () => 'C', prefix: false, includes: ['*'] }
        ]
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (msg: any) => { throw new Error(`Unexpected error: ${msg}`); }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('view with cube in different roots - should pass', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A', prefix: false, includes: ['*'] },
          { joinPath: () => 'B.A', prefix: false, includes: ['*'] }
        ]
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (msg: any) => { throw new Error(`Unexpected error: ${msg}`); }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('view with same cube in middle of different root paths - should pass', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A.B.X', prefix: false, includes: ['*'] },
          { joinPath: () => 'C.B.Y', prefix: false, includes: ['*'] }
        ]
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (msg: any) => { throw new Error(`Unexpected error: ${msg}`); }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('view with complex transitive conflicts - multiple cubes with conflicts - should fail', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A.B.C', prefix: false, includes: ['*'] },
          { joinPath: () => 'A.D.C', prefix: false, includes: ['*'] }
        ]
      };

      let errorMessage = '';
      cubeValidator.validate(cube, {
        error: (msg: any) => { errorMessage = msg; }
      } as any);

      // All intermediate and leaf cubes that appear in multiple paths should be reported
      expect(errorMessage).toContain('has multiple paths to \'C\'');
      expect(errorMessage).toContain('within root \'A\'');
      expect(errorMessage).toContain('\'A.B.C\'');
      expect(errorMessage).toContain('\'A.D.C\'');
    });

    it('view with same cube at different depths in different roots - should pass', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A.B', prefix: false, includes: ['*'] },
          { joinPath: () => 'C.D.E.B', prefix: false, includes: ['*'] }
        ]
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (msg: any) => { throw new Error(`Unexpected error: ${msg}`); }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('view with no transitive conflicts - different cubes in paths - should pass', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A.B.C', prefix: false, includes: ['*'] },
          { joinPath: () => 'D.E.F', prefix: false, includes: ['*'] }
        ]
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (msg: any) => { throw new Error(`Unexpected error: ${msg}`); }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });

    it('view with same cube in different roots - should pass', async () => {
      const cubeValidator = new CubeValidator(new CubeSymbols());
      const cube = {
        name: 'test_view',
        isView: true,
        fileName: 'fileName',
        cubes: [
          { joinPath: () => 'A.B.C', prefix: false, includes: ['*'] },
          { joinPath: () => 'D.B.C', prefix: false, includes: ['*'] }
        ]
      };

      const validationResult = cubeValidator.validate(cube, {
        error: (msg: any) => { throw new Error(`Unexpected error: ${msg}`); }
      } as any);

      expect(validationResult.error).toBeFalsy();
    });
  });
});
