import {
  clientContextValidation,
  dataModelsValidation,
  embeddableValidation,
  formatIssue,
  securityContextValidation,
} from "./validate";
import * as fs from "node:fs/promises";

const startMock = {
  succeed: vi.fn(),
  fail: vi.fn(),
};

vi.mock("@embeddable.com/sdk-utils", async (importOriginal) => {
  const actual =
    await importOriginal<typeof import("@embeddable.com/sdk-utils")>();
  return {
    ...actual,
    errorFormatter: vi.fn((issues) =>
      issues.map((issue: any) => issue.message || "Validation error"),
    ),
    findFiles: vi.fn(),
  };
});

const failMock = vi.fn();

const validYaml = `cubes:
  - name: customers
    title: My customers
    data_source: default
    sql_table: public.customers

    dimensions:
      - name: id
        sql: id
        type: number
        primary_key: true`;

const invalidYaml = `${validYaml}
    measures:
      - name: count
        type: count
        title: '# of customers'
      - name: test
        type: number
        sql: {count} / 10.0`;

const securityContextYaml = `
- name: Example customer 1
  securityContext:
    country: United States
  environment: default`;

const clientContextYaml = `
- name: blue
  clientContext:
    color: blue`;

const clientContextWithVariablesYaml = `
- name: US
  clientContext:
    theme: default
    locale: en-US
  variables:
    currency: USD
    country: United States`;

const clientContextWithEmptyVariablesYaml = `
- name: UK
  clientContext:
    theme: default
    locale: en-GB
  variables: {}`;

const clientContextWithVariousVariableTypesYaml = `
- name: Complex
  clientContext:
    theme: dark
  variables:
    stringVar: "hello"
    numberVar: 42
    booleanVar: true
    arrayVar: [1, 2, 3]
    objectVar:
      nested: "value"`;

vi.mock("ora", () => ({
  default: () => ({
    start: vi.fn().mockImplementation(() => startMock),
    info: vi.fn(),
    fail: failMock,
  }),
}));

vi.mock("node:fs/promises", () => ({
  readFile: vi.fn(),
  writeFile: vi.fn(),
  access: vi.fn(),
  mkdir: vi.fn(),
}));

describe("validate", () => {
  describe("dataModelsValidation", () => {
    it("should return an empty array if the data models are valid", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return validYaml;
      });
      const filesList: [string, string][] = [
        ["valid-cube.yaml", "path/to/file"],
      ];
      const result = await dataModelsValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should return an array of error messages if the data models are invalid", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return "";
      });
      const filesList: [string, string][] = [
        ["invalid-cube.yaml", "path/to/file"],
      ];
      const result = await dataModelsValidation(filesList);
      expect(result).toEqual([
        "path/to/file: At least one cubes or views must be defined",
      ]);
    });

    it("should return an array of error messages if the data models parsing fails", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return invalidYaml;
      });
      const filesList: [string, string][] = [
        ["invalid-cube.yaml", "path/to/file"],
      ];
      const result = await dataModelsValidation(filesList);
      expect(result).toMatchInlineSnapshot(`
        [
          "path/to/file: Unexpected scalar at node end at line 18, column 22:

                sql: {count} / 10.0
                             ^^^^^^
        ",
        ]
      `);
    });
  });

  describe("securityContextValidation", () => {
    it("should return an empty array if the security context is valid", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return securityContextYaml;
      });
      const filesList: [string, string][] = [
        ["valid-security-context.json", "path/to/file"],
      ];
      const result = await securityContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should return an array of error messages if the security context is invalid", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return `${securityContextYaml} ${securityContextYaml}`;
      });
      const filesList: [string, string][] = [
        ["invalid-security-context.json", "path/to/file"],
      ];
      const result = await securityContextValidation(filesList);
      expect(result).toEqual([
        'path/to/file: security context with name "Example customer 1" already exists',
      ]);
    });
  });

  describe("clientContextValidation", () => {
    it("should return an empty array if the client context is valid", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return clientContextYaml;
      });
      const filesList: [string, string][] = [
        ["valid-client-context.json", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should return an array of error messages if the client context is invalid", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return `${clientContextYaml} ${clientContextYaml}`;
      });
      const filesList: [string, string][] = [
        ["invalid-client-context.json", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([
        'path/to/file: client context with name "blue" already exists',
      ]);
    });

    it("should validate client context with variables field", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return clientContextWithVariablesYaml;
      });
      const filesList: [string, string][] = [
        ["valid-client-context-with-variables.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should validate client context with empty variables object", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return clientContextWithEmptyVariablesYaml;
      });
      const filesList: [string, string][] = [
        ["valid-client-context-empty-variables.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should validate client context with various variable types", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return clientContextWithVariousVariableTypesYaml;
      });
      const filesList: [string, string][] = [
        ["valid-client-context-various-types.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should validate client context without variables field (backward compatibility)", async () => {
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return clientContextYaml;
      });
      const filesList: [string, string][] = [
        ["valid-client-context-no-variables.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should reject client context with variables as non-object type", async () => {
      const invalidYaml = `
- name: Invalid
  clientContext:
    theme: default
  variables: "not an object"`;

      vi.mocked(fs.readFile).mockImplementation(async () => {
        return invalidYaml;
      });
      const filesList: [string, string][] = [
        ["invalid-variables-type.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should reject client context with variables as array", async () => {
      const invalidYaml = `
- name: Invalid
  clientContext:
    theme: default
  variables:
    - item1
    - item2`;

      vi.mocked(fs.readFile).mockImplementation(async () => {
        return invalidYaml;
      });
      const filesList: [string, string][] = [
        ["invalid-variables-array.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should validate client context with variables containing null values", async () => {
      const yamlWithNull = `
- name: WithNull
  clientContext:
    theme: default
  variables:
    nullableVar: null
    stringVar: "value"`;

      vi.mocked(fs.readFile).mockImplementation(async () => {
        return yamlWithNull;
      });
      const filesList: [string, string][] = [
        ["valid-with-null.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should validate multiple client contexts with variables in same file", async () => {
      const multipleContextsYaml = `
- name: US
  clientContext:
    theme: default
  variables:
    currency: USD
- name: UK
  clientContext:
    theme: default
  variables:
    currency: GBP`;

      vi.mocked(fs.readFile).mockImplementation(async () => {
        return multipleContextsYaml;
      });
      const filesList: [string, string][] = [
        ["multiple-contexts.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should validate client context with variables having special characters in keys", async () => {
      const yamlWithSpecialKeys = `
- name: SpecialKeys
  clientContext:
    theme: default
  variables:
    "key-with-dashes": "value1"
    "key_with_underscores": "value2"
    "key.with.dots": "value3"
    "key with spaces": "value4"`;

      vi.mocked(fs.readFile).mockImplementation(async () => {
        return yamlWithSpecialKeys;
      });
      const filesList: [string, string][] = [
        ["special-keys.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should validate client context with deeply nested variables", async () => {
      const yamlWithNested = `
- name: Nested
  clientContext:
    theme: default
  variables:
    level1:
      level2:
        level3: "deep value"
        array: [1, 2, 3]
      simple: "value"`;

      vi.mocked(fs.readFile).mockImplementation(async () => {
        return yamlWithNested;
      });
      const filesList: [string, string][] = [
        ["nested-variables.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });

    it("should validate client context with variables and canvas together", async () => {
      const yamlWithBoth = `
- name: Complete
  clientContext:
    theme: default
  variables:
    currency: USD
  canvas:
    width: 800`;

      vi.mocked(fs.readFile).mockImplementation(async () => {
        return yamlWithBoth;
      });
      const filesList: [string, string][] = [
        ["complete-context.yaml", "path/to/file"],
      ];
      const result = await clientContextValidation(filesList);
      expect(result).toEqual([]);
    });
  });

  describe("embeddableValidation", () => {
    const validEmbeddableYaml = `
embeddables:
  - name: my-embeddable
    title: My Embeddable
    variables:
      - name: date-range
        type: timeRange
        array: false
        defaultValue:
          from: "2024-01-01"
          to: "2024-12-31"
    datasets:
      - name: filtered data
        model: daily_listens
        filters:
          - member: daily_listens.age_group
            operator: equals
            value: date-range
            valueType: VARIABLE
    widgets:
      - component: MultiSelectFieldPro
        position:
          x: 0
          y: 3
        dimensions:
          width: 7
          height: 3
        inputs:
          - input: title
            inputType: string
            value: "Age Group"
            valueType: VALUE
          - input: ds
            inputType: dataset
            valueType: VALUE
            value: filtered data
        events:
          - event: onChange
            action: SET_VARIABLE
            config:
              variable: date-range
              sourceType: EVENT_PROPERTY
              sourceValue: value`;

    const minimalEmbeddableYaml = `
embeddables:
  - name: minimal-dash
    title: Minimal`;

    it("should return an empty array for a valid embeddable file", async () => {
      vi.mocked(fs.readFile).mockImplementation(
        async () => validEmbeddableYaml,
      );
      const filesList: [string, string][] = [
        ["test.embeddable.yaml", "path/to/test.embeddable.yaml"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should return an empty array for a minimal embeddable file", async () => {
      vi.mocked(fs.readFile).mockImplementation(
        async () => minimalEmbeddableYaml,
      );
      const filesList: [string, string][] = [
        ["test.embeddable.yaml", "path/to/test.embeddable.yaml"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should reject missing root embeddables key", async () => {
      const yaml = `
name: bad
title: Bad`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should reject empty embeddables array", async () => {
      const yaml = `
embeddables: []`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["empty.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should reject unrecognised top-level keys", async () => {
      const yaml = `
embeddables:
  - name: test
    title: Test
unknownKey: bad`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should reject unrecognised keys on an embeddable item", async () => {
      const yaml = `
embeddables:
  - name: test
    title: Test
    bogus: true`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should detect duplicate embeddable names within a file", async () => {
      const yaml = `
embeddables:
  - name: duplicate-name
    title: First
  - name: duplicate-name
    title: Second`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["dup.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: embeddable with name "duplicate-name" already exists',
      ]);
    });

    it("should detect duplicate embeddable names across files", async () => {
      const yaml1 = `
embeddables:
  - name: shared-name
    title: File One`;
      const yaml2 = `
embeddables:
  - name: shared-name
    title: File Two`;

      let callCount = 0;
      vi.mocked(fs.readFile).mockImplementation(async () => {
        return callCount++ === 0 ? yaml1 : yaml2;
      });
      const filesList: [string, string][] = [
        ["a.embeddable.yaml", "path/to/a.embeddable.yaml"],
        ["b.embeddable.yaml", "path/to/b.embeddable.yaml"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/b.embeddable.yaml: embeddable with name "shared-name" already exists',
      ]);
    });

    it("should reject invalid valueType enum values", async () => {
      const yaml = `
embeddables:
  - name: test
    widgets:
      - component: Comp
        position: { x: 0, y: 0 }
        dimensions: { width: 1, height: 1 }
        inputs:
          - input: foo
            inputType: string
            value: bar
            valueType: INVALID`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should reject invalid action enum values", async () => {
      const yaml = `
embeddables:
  - name: test
    variables:
      - name: v1
        type: string
    widgets:
      - component: Comp
        position: { x: 0, y: 0 }
        dimensions: { width: 1, height: 1 }
        events:
          - event: onClick
            action: INVALID_ACTION
            config:
              variable: v1
              sourceType: EVENT_PROPERTY
              sourceValue: val`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should reject invalid sourceType enum values", async () => {
      const yaml = `
embeddables:
  - name: test
    variables:
      - name: v1
        type: string
    widgets:
      - component: Comp
        position: { x: 0, y: 0 }
        dimensions: { width: 1, height: 1 }
        events:
          - event: onClick
            action: SET_VARIABLE
            config:
              variable: v1
              sourceType: BADTYPE
              sourceValue: val`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should detect dataset filter referencing undefined variable", async () => {
      const yaml = `
embeddables:
  - name: test
    variables:
      - name: existing-var
        type: string
    datasets:
      - name: ds1
        model: my_model
        filters:
          - member: my_model.field
            operator: equals
            value: nonexistent-var
            valueType: VARIABLE`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["ref.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: dataset "ds1" references undefined variable "nonexistent-var"',
      ]);
    });

    it("should not flag dataset filter with valueType VALUE", async () => {
      const yaml = `
embeddables:
  - name: test
    datasets:
      - name: ds1
        model: my_model
        filters:
          - member: my_model.field
            operator: equals
            value: some-literal
            valueType: VALUE`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["ok.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should detect widget input referencing undefined variable", async () => {
      const yaml = `
embeddables:
  - name: test
    variables:
      - name: known-var
        type: string
    widgets:
      - component: Comp
        position: { x: 0, y: 0 }
        dimensions: { width: 1, height: 1 }
        inputs:
          - input: myInput
            inputType: string
            value: unknown-var
            valueType: VARIABLE`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["ref.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: widget "Comp" input "myInput" references undefined variable "unknown-var"',
      ]);
    });

    it("should detect widget input referencing undefined dataset", async () => {
      const yaml = `
embeddables:
  - name: test
    datasets:
      - name: real-dataset
        model: my_model
    widgets:
      - component: Comp
        position: { x: 0, y: 0 }
        dimensions: { width: 1, height: 1 }
        inputs:
          - input: ds
            inputType: dataset
            value: fake-dataset
            valueType: VALUE`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["ref.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: widget "Comp" input "ds" references undefined dataset "fake-dataset"',
      ]);
    });

    it("should detect SET_VARIABLE event referencing undefined variable", async () => {
      const yaml = `
embeddables:
  - name: test
    variables:
      - name: real-var
        type: string
    widgets:
      - component: Comp
        position: { x: 0, y: 0 }
        dimensions: { width: 1, height: 1 }
        events:
          - event: onChange
            action: SET_VARIABLE
            config:
              variable: missing-var
              sourceType: EVENT_PROPERTY
              sourceValue: value`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["ref.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: widget "Comp" event "onChange" references undefined variable "missing-var"',
      ]);
    });

    it("should detect overlapping widgets in an embeddable", async () => {
      const yaml = `
embeddables:
  - name: Test-webc
    widgets:
      - component: DateRangePickerCustomPro
        position: { x: 0, y: 0 }
        dimensions: { width: 4, height: 7 }
      - component: BarChartDefaultPro
        position: { x: 0, y: 2 }
        dimensions: { width: 12, height: 15 }`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["overlap.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);

      expect(result).toEqual([
        'path/to/file: embeddable "Test-webc" widgets "DateRangePickerCustomPro" and "BarChartDefaultPro" overlap. "DateRangePickerCustomPro" occupies x 0-4, y 0-7; "BarChartDefaultPro" occupies x 0-12, y 2-17.',
      ]);
    });

    it("should allow widgets that touch edges without overlapping", async () => {
      const yaml = `
embeddables:
  - name: Test-webc
    widgets:
      - component: DateRangePickerCustomPro
        position: { x: 0, y: 0 }
        dimensions: { width: 4, height: 7 }
      - component: BarChartDefaultPro
        position: { x: 0, y: 7 }
        dimensions: { width: 12, height: 15 }`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["valid.embeddable.yaml", "path/to/file"],
      ];
      const result = await embeddableValidation(filesList);

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

    it("should ignore overlap checks for widgets with non-positive dimensions", async () => {
      const yaml = `
embeddables:
  - name: Test-webc
    widgets:
      - component: EmptyWidthWidget
        position: { x: 0, y: 0 }
        dimensions: { width: 0, height: 7 }
      - component: BarChartDefaultPro
        position: { x: 0, y: 2 }
        dimensions: { width: 12, height: 15 }`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["non-positive-dimensions.embeddable.yaml", "path/to/file"],
      ];
      const result = await embeddableValidation(filesList);

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

    it("should detect customCanvas referencing undefined dataset", async () => {
      const yaml = `
embeddables:
  - name: test
    datasets:
      - name: real-ds
        model: my_model
    customCanvas:
      datasets:
        - dataset: bogus-ds`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["ref.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: customCanvas references undefined dataset "bogus-ds"',
      ]);
    });

    it("should reject template without key", async () => {
      const yaml = `
embeddables:
  - name: test
    customCanvas:
      templates:
        - name: Real Template
          component: SomeComp`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["nokey.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
      expect(result[0]).toContain("key");
    });

    it("should detect duplicate template keys", async () => {
      const yaml = `
embeddables:
  - name: test
    customCanvas:
      templates:
        - key: shared
          name: First
          component: CompA
        - key: shared
          name: Second
          component: CompB`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["dupkey.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: customCanvas has duplicate template key "shared"',
      ]);
    });

    it("should resolve starterCanvas reference by template key, not name", async () => {
      const yaml = `
embeddables:
  - name: test
    customCanvas:
      templates:
        - key: my-template
          name: Friendly Display Name
          component: SomeComp
      starterCanvas:
        widgets:
          - template: my-template
            dimensions: { width: 7, height: 3 }`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bykey.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should detect starterCanvas referencing template by name instead of key", async () => {
      const yaml = `
embeddables:
  - name: test
    customCanvas:
      templates:
        - key: my-template
          name: Friendly Display Name
          component: SomeComp
      starterCanvas:
        widgets:
          - template: Friendly Display Name
            dimensions: { width: 7, height: 3 }`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["byname.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: starterCanvas references undefined template "Friendly Display Name"',
      ]);
    });

    it("should detect starterCanvas referencing undefined template", async () => {
      const yaml = `
embeddables:
  - name: test
    customCanvas:
      templates:
        - key: real-template
          name: Real Template
          component: SomeComp
      starterCanvas:
        widgets:
          - template: Missing Template
            dimensions: { width: 7, height: 3 }`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["ref.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([
        'path/to/file: starterCanvas references undefined template "Missing Template"',
      ]);
    });

    it("should handle YAML parse errors gracefully", async () => {
      vi.mocked(fs.readFile).mockImplementation(
        async () => ":\ninvalid: [yaml: {{{",
      );
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should validate a full example with customCanvas and templates", async () => {
      const yaml = `
embeddables:
  - name: full-example
    title: Full Example
    variables:
      - name: country
        type: string
    datasets:
      - name: main-data
        model: users
    widgets:
      - component: Table
        position: { x: 0, y: 0 }
        dimensions: { width: 12, height: 6 }
        inputs:
          - input: ds
            inputType: dataset
            value: main-data
            valueType: VALUE
        events:
          - event: onClick
            action: DRILLDOWN
            config:
              embeddable: detail-view
              variableOverrides:
                - variable: country
                  sourceType: EVENT_PROPERTY
                  sourceValue: chosenItem
    customCanvas:
      datasets:
        - dataset: main-data
      templates:
        - key: bar-chart
          name: Bar Chart
          component: BarComp
          description: A bar chart
          icon: bar_chart
          inputs:
            - input: ds
              value: main-data
              valueType: VALUE
      starterCanvas:
        widgets:
          - template: bar-chart
            dimensions: { width: 6, height: 4 }`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["full.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should accept array: true on a template input", async () => {
      const yaml = `
embeddables:
  - name: test
    customCanvas:
      templates:
        - key: my-chart
          name: My Chart
          component: ChartComp
          inputs:
            - input: ds
              value: some-ds
              array: true`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["array.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should accept visible on a template input", async () => {
      const yaml = `
embeddables:
  - name: test
    customCanvas:
      templates:
        - key: my-chart
          name: My Chart
          component: ChartComp
          inputs:
            - input: ds
              value: some-ds
              visible: false
            - input: title
              value: Hello
              visible: true`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["visible.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should reject non-boolean visible on a template input", async () => {
      const yaml = `
embeddables:
  - name: test
    customCanvas:
      templates:
        - key: my-chart
          name: My Chart
          component: ChartComp
          inputs:
            - input: ds
              value: some-ds
              visible: "yes"`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["visible-bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should validate dataset filter with input config containing order", async () => {
      const yaml = `
embeddables:
  - name: test
    datasets:
      - name: ds1
        model: my_model
    widgets:
      - component: Table
        position: { x: 0, y: 0 }
        dimensions: { width: 12, height: 6 }
        inputs:
          - input: ds
            inputType: dataset
            value: ds1
            valueType: VALUE
            config:
              filters: []
              limit: 10
              order:
                - member: my_model.field
                  direction: asc`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["config.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should reject invalid order direction", async () => {
      const yaml = `
embeddables:
  - name: test
    widgets:
      - component: Table
        position: { x: 0, y: 0 }
        dimensions: { width: 12, height: 6 }
        inputs:
          - input: ds
            inputType: dataset
            value: ds1
            valueType: VALUE
            config:
              order:
                - member: my_model.field
                  direction: sideways`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
    });

    it("should validate nested config inputs on widgets", async () => {
      const yaml = `
embeddables:
  - name: test
    widgets:
      - component: DimField
        position: { x: 0, y: 0 }
        dimensions: { width: 4, height: 2 }
        inputs:
          - input: dim
            inputType: dimension
            value: my_model.field
            valueType: VALUE
            config:
              inputs:
                - input: suffix
                  value: some suffix
                  valueType: VALUE`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["nested.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should validate input with array flag and config.dataset with parentValue on nested inputs", async () => {
      const yaml = `
embeddables:
  - name: my-embeddable-new
    title: My Embeddable updated
    variables:
      - name: date-range
        type: timeRange
        array: false
        defaultValue: { relativeTimeString: 'Last 30 days' }
    datasets:
      - name: myDataset
        model: orders
    widgets:
      - component: BarChartDefaultPro
        position: { x: 0, y: 2 }
        dimensions: { width: 12, height: 15 }
        inputs:
          - input: dataset
            inputType: dataset
            valueType: VALUE
            value: myDataset
          - input: measures
            inputType: measure
            valueType: VALUE
            array: true
            value:
              - customers.count
              - orders.count
            config:
              dataset: dataset
              inputs:
                - input: prefix
                  value: "Test 2"
                  parentValue: customers.count
                  valueType: VALUE
                - input: suffix
                  value: "Suf 1"
                  parentValue: orders.count
                  valueType: VALUE
          - input: dimension
            inputType: dimension
            valueType: VALUE
            value: orders.created_at
            config:
              dataset: dataset
              inputs:
                - input: granularity
                  value: "month"
                  valueType: VALUE`;

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["array-parent.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should accept valid operation values and reject invalid ones on variables", async () => {
      const validYaml = `
embeddables:
  - name: test
    variables:
      - name: v1
        type: string
        operation: NO_FILTER
      - name: v2
        type: string
        operation: VALUE`;

      vi.mocked(fs.readFile).mockImplementation(async () => validYaml);
      const filesList: [string, string][] = [
        ["test.embeddable.yaml", "path/to/file"],
      ];
      expect((await embeddableValidation(filesList)).map(formatIssue)).toEqual(
        [],
      );

      const invalidYaml = `
embeddables:
  - name: test
    variables:
      - name: v1
        type: string
        operation: INVALID`;

      vi.mocked(fs.readFile).mockImplementation(async () => invalidYaml);
      const errors = (await embeddableValidation(filesList)).map(formatIssue);
      expect(errors.length).toBeGreaterThan(0);
      expect(errors[0]).toContain("path/to/file");
    });

    it("should return no errors for an empty file list", async () => {
      const filesList: [string, string][] = [];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result).toEqual([]);
    });

    it("should reference embeddable name instead of index in error messages", async () => {
      const yaml = `
embeddables:
  - name: My Dashboard
    widgets:
      - component: BarChart
        position: { x: 0, y: 0 }
        dimensions: { width: 4, height: 2 }
        inputs:
          - input: showValueLabel
            value: true
            valueType: VALUE`;
      // inputType is required but missing — should name the embeddable and widget

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["bad.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("Embeddable 'My Dashboard'");
      expect(result[0]).toContain("widget 'BarChart'");
      expect(result[0]).toContain("input 'showValueLabel'");
      expect(result[0]).not.toMatch(/embeddables\[0\]/);
      expect(result[0]).not.toMatch(/widgets\[0\]/);
      expect(result[0]).not.toMatch(/inputs\[0\]/);
    });

    it("should reference the correct names when error is on a later index", async () => {
      const yaml = `
embeddables:
  - name: First Dashboard
    widgets:
      - component: PieChart
        position: { x: 0, y: 0 }
        dimensions: { width: 4, height: 2 }
        inputs:
          - input: title
            inputType: string
            value: My Title
            valueType: VALUE
          - input: dataset
            inputType: dataset
            value: ds1
            valueType: VALUE
          - input: metricLabel
            value: Revenue
            valueType: VALUE`;
      // inputType missing on the third input (index 2)

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["indexed.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("Embeddable 'First Dashboard'");
      expect(result[0]).toContain("widget 'PieChart'");
      expect(result[0]).toContain("input 'metricLabel'");
      expect(result[0]).not.toMatch(/inputs\[2\]/);
    });

    it("should fall back to index notation when name fields are absent in raw data", async () => {
      // An embeddable with no 'name' field — schema will reject it, but the
      // formatter should degrade gracefully (e.g. keep embeddables[0]).
      const yaml = `
embeddables:
  - title: Unnamed`;
      // 'name' is required, so there will be a validation error at embeddables[0]

      vi.mocked(fs.readFile).mockImplementation(async () => yaml);
      const filesList: [string, string][] = [
        ["noname.embeddable.yaml", "path/to/file"],
      ];
      const result = (await embeddableValidation(filesList)).map(formatIssue);
      expect(result.length).toBeGreaterThan(0);
      expect(result[0]).toContain("path/to/file");
      // Should not crash — just degrade to index notation
      expect(result[0]).toMatch(/embeddables\[0\]/);
    });
  });
});
