import outdent from 'outdent';
import each from 'jest-each';
import * as path from 'path';

import { lintDocument } from '../src/lint';

import { parseYamlToDocument, replaceSourceWithRef, makeConfigForRuleset } from './utils';
import { BaseResolver, Document } from '../src/resolve';
import { listOf } from '../src/types';
import { Oas3RuleSet } from '../src/oas-types';

describe('walk order', () => {
  it('should run visitors', async () => {
    const visitors = {
      DefinitionRoot: {
        enter: jest.fn(),
        leave: jest.fn(),
      },
      Info: {
        enter: jest.fn(),
        leave: jest.fn(),
      },
      Contact: {
        enter: jest.fn(),
        leave: jest.fn(),
      },
      License: {
        enter: jest.fn(),
        leave: jest.fn(),
      },
    };

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return visitors;
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        info:
          contact: {}
          license: {}
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(testRuleSet.test).toBeCalledTimes(1);
    for (const fns of Object.values(visitors)) {
      expect(fns.enter).toBeCalled();
      expect(fns.leave).toBeCalled();
    }
  });

  it('should run nested visitors correctly', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Operation: {
            enter: jest.fn((op) => calls.push(`enter operation: ${op.operationId}`)),
            leave: jest.fn((op) => calls.push(`leave operation: ${op.operationId}`)),
            Parameter: {
              enter: jest.fn((param, _ctx, parents) =>
                calls.push(
                  `enter operation ${parents.Operation.operationId} > param ${param.name}`,
                ),
              ),
              leave: jest.fn((param, _ctx, parents) =>
                calls.push(
                  `leave operation ${parents.Operation.operationId} > param ${param.name}`,
                ),
              ),
            },
          },
          Parameter: {
            enter: jest.fn((param) => calls.push(`enter param ${param.name}`)),
            leave: jest.fn((param) => calls.push(`leave param ${param.name}`)),
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        info:
          contact: {}
          license: {}
        paths:
          /pet:
            parameters:
              - name: path-param
            get:
              operationId: get
              parameters:
                - name: get_a
                - name: get_b
            post:
              operationId: post
              parameters:
                - name: post_a

      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter param path-param",
        "leave param path-param",
        "enter operation: get",
        "enter operation get > param get_a",
        "enter param get_a",
        "leave param get_a",
        "leave operation get > param get_a",
        "enter operation get > param get_b",
        "enter param get_b",
        "leave param get_b",
        "leave operation get > param get_b",
        "leave operation: get",
        "enter operation: post",
        "enter operation post > param post_a",
        "enter param post_a",
        "leave param post_a",
        "leave operation post > param post_a",
        "leave operation: post",
      ]
    `);
  });

  it('should run nested visitors correctly oas2', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Operation: {
            enter: jest.fn((op) => calls.push(`enter operation: ${op.operationId}`)),
            leave: jest.fn((op) => calls.push(`leave operation: ${op.operationId}`)),
            Parameter: {
              enter: jest.fn((param, _ctx, parents) =>
                calls.push(
                  `enter operation ${parents.Operation.operationId} > param ${param.name}`,
                ),
              ),
              leave: jest.fn((param, _ctx, parents) =>
                calls.push(
                  `leave operation ${parents.Operation.operationId} > param ${param.name}`,
                ),
              ),
            },
          },
          Parameter: {
            enter: jest.fn((param) => calls.push(`enter param ${param.name}`)),
            leave: jest.fn((param) => calls.push(`leave param ${param.name}`)),
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        swagger: "2.0"
        info:
          contact: {}
          license: {}
        paths:
          /pet:
            parameters:
              - name: path-param
            get:
              operationId: get
              parameters:
                - name: get_a
                - name: get_b
            post:
              operationId: post
              parameters:
                - name: post_a

      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet, undefined, 'oas2'),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter param path-param",
        "leave param path-param",
        "enter operation: get",
        "enter operation get > param get_a",
        "enter param get_a",
        "leave param get_a",
        "leave operation get > param get_a",
        "enter operation get > param get_b",
        "enter param get_b",
        "leave param get_b",
        "leave operation get > param get_b",
        "leave operation: get",
        "enter operation: post",
        "enter operation post > param post_a",
        "enter param post_a",
        "leave param post_a",
        "leave operation post > param post_a",
        "leave operation: post",
      ]
    `);
  });

  it('should resolve refs', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Operation: {
            enter: jest.fn((op) => calls.push(`enter operation: ${op.operationId}`)),
            leave: jest.fn((op) => calls.push(`leave operation: ${op.operationId}`)),
            Parameter: {
              enter: jest.fn((param, _ctx, parents) =>
                calls.push(
                  `enter operation ${parents.Operation.operationId} > param ${param.name}`,
                ),
              ),
              leave: jest.fn((param, _ctx, parents) =>
                calls.push(
                  `leave operation ${parents.Operation.operationId} > param ${param.name}`,
                ),
              ),
            },
          },
          Parameter: {
            enter: jest.fn((param) => calls.push(`enter param ${param.name}`)),
            leave: jest.fn((param) => calls.push(`leave param ${param.name}`)),
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        info:
          contact: {}
          license: {}
        paths:
          /pet:
            get:
              operationId: get
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
            post:
              operationId: post
              parameters:
                - $ref: '#/components/parameters/shared_a'
        components:
          parameters:
            shared_a:
              name: shared-a
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter operation: get",
        "enter operation get > param shared-a",
        "enter param shared-a",
        "leave param shared-a",
        "leave operation get > param shared-a",
        "enter operation get > param get_b",
        "enter param get_b",
        "leave param get_b",
        "leave operation get > param get_b",
        "leave operation: get",
        "enter operation: post",
        "enter operation post > param shared-a",
        "leave operation post > param shared-a",
        "leave operation: post",
      ]
    `);
  });

  it('should visit with context same refs with gaps in visitor simple', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          PathItem: {
            Parameter: {
              enter: jest.fn((param, _ctx, parents) =>
                calls.push(`enter path ${parents.PathItem.id} > param ${param.name}`),
              ),
            },
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            id: pet
            parameters:
              $ref: '#/components/fake_parameters_list'
            get:
              operationId: get
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
          /dog:
            id: dog
            post:
              operationId: post
              parameters:
                - $ref: '#/components/parameters/shared_a'
        components:
          fake_parameters_list:
            - name: path-param
          parameters:
            shared_a:
              name: shared-a
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter path pet > param path-param",
        "enter path pet > param shared-a",
        "enter path pet > param get_b",
        "enter path dog > param shared-a",
      ]
    `);
  });

  it('should correctly visit more specific visitor', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          PathItem: {
            Parameter: {
              enter: jest.fn((param, _ctx, parents) =>
                calls.push(`enter path ${parents.PathItem.id} > param ${param.name}`),
              ),
            },
            Operation: {
              Parameter: {
                enter: jest.fn((param, _ctx, parents) =>
                  calls.push(
                    `enter operation ${parents.Operation.operationId} > param ${param.name}`,
                  ),
                ),
              },
            },
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            id: pet
            parameters:
             - name: path-param
            get:
              operationId: get
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
                - name: get_c
          /dog:
            id: dog
            post:
              operationId: post
              parameters:
                - $ref: '#/components/parameters/shared_b'
        components:
          parameters:
            shared_a:
              name: shared-a
            shared_b:
              name: shared-b
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter path pet > param path-param",
        "enter operation get > param shared-a",
        "enter operation get > param get_b",
        "enter operation get > param get_c",
        "enter operation post > param shared-b",
      ]
    `);
  });

  it('should visit with context same refs with gaps in visitor and nested rule', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          PathItem: {
            Parameter: {
              enter: jest.fn((param, _ctx, parents) =>
                calls.push(`enter path ${parents.PathItem.id} > param ${param.name}`),
              ),
              leave: jest.fn((param, _ctx, parents) =>
                calls.push(`leave path ${parents.PathItem.id} > param ${param.name}`),
              ),
            },
            Operation(op, _ctx, parents) {
              calls.push(`enter path ${parents.PathItem.id} > op ${op.operationId}`);
            },
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            id: pet
            parameters:
             - name: path-param
            get:
              operationId: get
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
          /dog:
            id: dog
            post:
              operationId: post
              parameters:
                - $ref: '#/components/parameters/shared_a'
        components:
          parameters:
            shared_a:
              name: shared-a
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter path pet > param path-param",
        "leave path pet > param path-param",
        "enter path pet > op get",
        "enter path pet > param shared-a",
        "leave path pet > param shared-a",
        "enter path pet > param get_b",
        "leave path pet > param get_b",
        "enter path dog > op post",
        "enter path dog > param shared-a",
        "leave path dog > param shared-a",
      ]
    `);
  });

  it('should visit and do not recurse for circular refs top-level', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Schema: jest.fn((schema: any) => calls.push(`enter schema ${schema.id}`)),
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            id: pet
            parameters:
             - name: path-param
               schema:
                 $ref: "#/components/parameters/shared_a"
        components:
          parameters:
            shared_a:
              id: 'shared_a'
              allOf:
                - $ref: "#/components/parameters/shared_a"
                - id: 'nested'
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter schema shared_a",
        "enter schema nested",
      ]
    `);
  });

  it('should visit and do not recurse for circular refs with context', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Parameter: {
            Schema: jest.fn((schema: any, _ctx, parents) =>
              calls.push(`enter param ${parents.Parameter.name} > schema ${schema.id}`),
            ),
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            id: pet
            parameters:
            - name: a
              schema:
                $ref: "#/components/parameters/shared_a"
            - name: b
              schema:
                $ref: "#/components/parameters/shared_a"
        components:
          parameters:
            shared_a:
              id: 'shared_a'
              properties:
                a:
                  id: a
              allOf:
                - $ref: "#/components/parameters/shared_a"
                - id: 'nested'
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter param a > schema shared_a",
        "enter param b > schema shared_a",
      ]
    `);
  });

  it('should correctly skip top level', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Operation: {
            skip: (op) => op.operationId === 'put',
            enter: jest.fn((op) => calls.push(`enter operation ${op.operationId}`)),
            leave: jest.fn((op) => calls.push(`leave operation ${op.operationId}`)),
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            get:
              operationId: get
            put:
              operationId: put
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter operation get",
        "leave operation get",
      ]
    `);
  });

  it('should correctly skip nested levels', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Operation: {
            skip: (op) => op.operationId === 'put',
            Parameter: jest.fn((param, _ctx, parents) =>
              calls.push(`enter operation ${parents.Operation.operationId} > param ${param.name}`),
            ),
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            get:
              operationId: get
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
                - name: get_c
            put:
              operationId: put
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
                - name: get_c
        components:
          parameters:
            shared_a:
              name: shared-a
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter operation get > param shared-a",
        "enter operation get > param get_b",
        "enter operation get > param get_c",
      ]
    `);
  });

  it('should correctly visit more specific visitor with skips', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          PathItem: {
            Parameter: {
              enter: jest.fn((param, _ctx, parents) =>
                calls.push(`enter path ${parents.PathItem.id} > param ${param.name}`),
              ),
              leave: jest.fn((param, _ctx, parents) =>
                calls.push(`leave path ${parents.PathItem.id} > param ${param.name}`),
              ),
            },
            Operation: {
              skip: (op) => op.operationId === 'put',
              Parameter: {
                enter: jest.fn((param, _ctx, parents) =>
                  calls.push(
                    `enter operation ${parents.Operation.operationId} > param ${param.name}`,
                  ),
                ),
                leave: jest.fn((param, _ctx, parents) =>
                  calls.push(
                    `leave operation ${parents.Operation.operationId} > param ${param.name}`,
                  ),
                ),
              },
            },
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            id: pet
            parameters:
             - name: path-param
            get:
              operationId: get
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
                - name: get_c
            put:
              operationId: put
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
                - name: get_c
          /dog:
            id: dog
            post:
              operationId: post
              parameters:
                - $ref: '#/components/parameters/shared_b'
        components:
          parameters:
            shared_a:
              name: shared-a
            shared_b:
              name: shared-b
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter path pet > param path-param",
        "leave path pet > param path-param",
        "enter operation get > param shared-a",
        "leave operation get > param shared-a",
        "enter operation get > param get_b",
        "leave operation get > param get_b",
        "enter operation get > param get_c",
        "leave operation get > param get_c",
        "enter operation post > param shared-b",
        "leave operation post > param shared-b",
      ]
    `);
  });

  it('should correctly visit with nested rules', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Schema: {
            Schema: {
              enter: jest.fn((schema: any, _ctx, parents) =>
                calls.push(`enter nested schema ${parents.Schema.id} > ${schema.id}`),
              ),
              leave: jest.fn((schema: any, _ctx, parents) =>
                calls.push(`leave nested schema ${parents.Schema.id} > ${schema.id}`),
              ),
            },
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            get:
              requestBody:
                content:
                  application/json:
                    schema:
                      id: inline-top
                      type: object
                      properties:
                        b:
                          $ref: "#/components/schemas/b"
                        a:
                          type: object
                          id: inline-nested-2
                          properties:
                            a:
                              id: inline-nested-nested-2
        components:
          schemas:
            b:
              id: inline-top
              type: object
              properties:
                a:
                  type: object
                  id: inline-nested
                  properties:
                    a:
                      id: inline-nested-nested
      `,
      'foobar.yaml',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter nested schema inline-top > inline-top",
        "enter nested schema inline-top > inline-nested",
        "enter nested schema inline-nested > inline-nested-nested",
        "leave nested schema inline-nested > inline-nested-nested",
        "leave nested schema inline-top > inline-nested",
        "leave nested schema inline-top > inline-top",
        "enter nested schema inline-top > inline-nested-2",
        "enter nested schema inline-nested-2 > inline-nested-nested-2",
        "leave nested schema inline-nested-2 > inline-nested-nested-2",
        "leave nested schema inline-top > inline-nested-2",
      ]
    `);
  });

  it('should correctly visit refs', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          ref(node, _, { node: target }) {
            calls.push(`enter $ref ${node.$ref} with target ${target?.name}`);
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
      openapi: 3.0.0
      paths:
        /pet:
          id: pet
          parameters:
           - name: path-param
          get:
            operationId: get
            parameters:
              - $ref: '#/components/parameters/shared_b'
          put:
            operationId: put
            parameters:
              - $ref: '#/components/parameters/shared_a'
        /dog:
          id: dog
          post:
            operationId: post
            schema:
              example:
                $ref: 123
            parameters:
              - $ref: '#/components/parameters/shared_a'
      components:
        parameters:
          shared_a:
            name: shared-a
          shared_b:
            name: shared-b
            schema:
              $ref: '#/components/parameters/shared_b'
      `,
      'foobar.yaml',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter $ref #/components/parameters/shared_b with target shared-b",
        "enter $ref #/components/parameters/shared_b with target shared-b",
        "enter $ref #/components/parameters/shared_a with target shared-a",
        "enter $ref #/components/parameters/shared_a with target shared-a",
      ]
    `);
  });

  it('should correctly visit refs', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          NamedSchemas: {
            Schema(node, { key }) {
              calls.push(`enter schema ${key}: ${node.type}`);
            },
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
      openapi: 3.0.0
      components:
        schemas:
          a:
            type: string
          b:
            type: number
      `,
      'foobar.yaml',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter schema a: string",
        "enter schema b: number",
      ]
    `);
  });

  it('should correctly visit any visitor', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          ref: {
            enter(ref: any) {
              calls.push(`enter ref ${ref.$ref}`);
            },
            leave(ref) {
              calls.push(`leave ref ${ref.$ref}`);
            },
          },
          any: {
            enter(_node: any, { type }) {
              calls.push(`enter ${type.name}`);
            },
            leave(_node, { type }) {
              calls.push(`leave ${type.name}`);
            },
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            id: pet
            parameters:
             - name: path-param
            get:
              operationId: get
              parameters:
                - $ref: '#/components/parameters/shared_a'
                - name: get_b
                - name: get_c
        components:
          parameters:
            shared_a:
              name: shared-a
          schemas:
            a:
              type: object
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter DefinitionRoot",
        "enter PathMap",
        "enter PathItem",
        "enter Parameter_List",
        "enter Parameter",
        "leave Parameter",
        "leave Parameter_List",
        "enter Operation",
        "enter Parameter_List",
        "enter ref #/components/parameters/shared_a",
        "enter Parameter",
        "leave Parameter",
        "leave ref #/components/parameters/shared_a",
        "enter Parameter",
        "leave Parameter",
        "enter Parameter",
        "leave Parameter",
        "leave Parameter_List",
        "leave Operation",
        "leave PathItem",
        "leave PathMap",
        "enter Components",
        "enter NamedParameters",
        "leave NamedParameters",
        "enter NamedSchemas",
        "enter Schema",
        "leave Schema",
        "leave NamedSchemas",
        "leave Components",
        "leave DefinitionRoot",
      ]
    `);
  });
});

describe('context.report', () => {
  it('should report errors correctly', async () => {
    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Parameter: {
            enter: jest.fn((param, ctx) => {
              if (param.name.indexOf('_') > -1) {
                ctx.report({
                  message: `Parameter name shouldn't contain '_: ${param.name}`,
                });
              }
            }),
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        info:
          contact: {}
          license: {}
        paths:
          /pet:
            parameters:
              - name: path-param
            get:
              operationId: get
              parameters:
                - name: get_a
                - name: get_b
            post:
              operationId: post
              parameters:
                - $ref: '#/components/parameters/shared_a'
        components:
          parameters:
            shared_a:
              name: shared_a
      `,
      'foobar.yaml',
    );

    const results = await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(results).toHaveLength(3);
    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
      Array [
        Object {
          "location": Array [
            Object {
              "pointer": "#/paths/~1pet/get/parameters/0",
              "reportOnKey": false,
              "source": "foobar.yaml",
            },
          ],
          "message": "Parameter name shouldn't contain '_: get_a",
          "ruleId": "test/test",
          "severity": "error",
          "suggest": Array [],
        },
        Object {
          "location": Array [
            Object {
              "pointer": "#/paths/~1pet/get/parameters/1",
              "reportOnKey": false,
              "source": "foobar.yaml",
            },
          ],
          "message": "Parameter name shouldn't contain '_: get_b",
          "ruleId": "test/test",
          "severity": "error",
          "suggest": Array [],
        },
        Object {
          "location": Array [
            Object {
              "pointer": "#/components/parameters/shared_a",
              "reportOnKey": false,
              "source": "foobar.yaml",
            },
          ],
          "message": "Parameter name shouldn't contain '_: shared_a",
          "ruleId": "test/test",
          "severity": "error",
          "suggest": Array [],
        },
      ]
    `);
  });

  it('should report errors correctly', async () => {
    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Parameter: {
            enter: jest.fn((param, ctx) => {
              if (param.name.indexOf('_') > -1) {
                ctx.report({
                  message: `Parameter name shouldn't contain '_: ${param.name}`,
                });
              }
            }),
          },
        };
      }),
    };

    const cwd = path.join(__dirname, 'fixtures/refs');
    const externalRefResolver = new BaseResolver();
    const document = (await externalRefResolver.resolveDocument(
      null,
      `${cwd}/openapi-with-external-refs.yaml`,
    )) as Document;

    if (document === null) {
      throw 'Should never happen';
    }

    const results = await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(results).toHaveLength(4);
    expect(replaceSourceWithRef(results, cwd)).toMatchInlineSnapshot(`
      Array [
        Object {
          "location": Array [
            Object {
              "pointer": "#/components/parameters/path-param",
              "reportOnKey": false,
              "source": "openapi-with-external-refs.yaml",
            },
          ],
          "message": "Parameter name shouldn't contain '_: path_param",
          "ruleId": "test/test",
          "severity": "error",
          "suggest": Array [],
        },
        Object {
          "location": Array [
            Object {
              "pointer": "#/components/parameters/param-a",
              "reportOnKey": false,
              "source": "openapi-with-external-refs.yaml",
            },
          ],
          "message": "Parameter name shouldn't contain '_: param_a",
          "ruleId": "test/test",
          "severity": "error",
          "suggest": Array [],
        },
        Object {
          "location": Array [
            Object {
              "pointer": "#/",
              "reportOnKey": false,
              "source": "param-c.yaml",
            },
          ],
          "message": "Parameter name shouldn't contain '_: param_c",
          "ruleId": "test/test",
          "severity": "error",
          "suggest": Array [],
        },
        Object {
          "location": Array [
            Object {
              "pointer": "#/",
              "reportOnKey": false,
              "source": "param-b.yaml",
            },
          ],
          "message": "Parameter name shouldn't contain '_: param_b",
          "ruleId": "test/test",
          "severity": "error",
          "suggest": Array [],
        },
      ]
    `);
  });
});

describe('context.resolve', () => {
  it('should resolve refs correctly', async () => {
    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          Schema: jest.fn((schema, { resolve }) => {
            if (schema.properties) {
              expect(schema.properties.a.$ref).toBeDefined();
              const { location, node } = resolve(schema.properties.a);
              expect(node).toMatchInlineSnapshot(`
                Object {
                  "type": "string",
                }
              `);
              expect(location?.pointer).toEqual('#/components/schemas/b');
              expect(location?.source).toStrictEqual(document.source);
            }
          }),
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        info:
          contact: {}
          license: {}
        paths: {}
        components:
          schemas:
            b:
              type: string
            a:
              type: object
              properties:
                a:
                  $ref: '#/components/schemas/b'
      `,
      'foobar.yaml',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });
  });
});

describe('type extensions', () => {
  each([
    ['3.0.0', 'oas3_0'],
    ['3.1.0', 'oas3_1'],
  ]).it('should correctly visit OpenAPI %s extended types', async (openapi, oas) => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      test: jest.fn(() => {
        return {
          any: {
            enter(_node: any, { type }) {
              calls.push(`enter ${type.name}`);
            },
            leave(_node, { type }) {
              calls.push(`leave ${type.name}`);
            },
          },
          XWebHooks: {
            enter(hook: any) {
              calls.push(`enter hook ${hook.name}`);
            },
            leave(hook) {
              calls.push(`leave hook ${hook.name}`);
            },
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: ${openapi}
        x-webhooks:
          name: test
          parameters:
            - name: a
      `,
      'foobar.yaml',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet, {
        typeExtension: {
          oas3(types, version) {
            expect(version).toEqual(oas);

            return {
              ...types,
              XWebHooks: {
                properties: {
                  parameters: listOf('Parameter'),
                },
              },
              DefinitionRoot: {
                ...types.DefinitionRoot,
                properties: {
                  ...types.DefinitionRoot.properties,
                  'x-webhooks': 'XWebHooks',
                },
              },
            };
          },
        },
      }),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter DefinitionRoot",
        "enter XWebHooks",
        "enter hook test",
        "enter Parameter_List",
        "enter Parameter",
        "leave Parameter",
        "leave Parameter_List",
        "leave hook test",
        "leave XWebHooks",
        "leave DefinitionRoot",
      ]
    `);
  });
});

describe('ignoreNextRules', () => {
  it('should correctly skip top level', async () => {
    const calls: string[] = [];

    const testRuleSet: Oas3RuleSet = {
      skip: jest.fn(() => {
        return {
          Operation: {
            enter: jest.fn((op, ctx) => {
              if (op.operationId === 'get') {
                ctx.ignoreNextVisitorsOnNode();
                calls.push(`enter and skip operation ${op.operationId}`);
              } else {
                calls.push(`enter and not skip operation ${op.operationId}`);
              }
            }),
            leave: jest.fn((op) => {
              if (op.operationId === 'get') {
                calls.push(`leave skipped operation ${op.operationId}`);
              } else {
                calls.push(`leave not skipped operation ${op.operationId}`);
              }
            }),
          },
        };
      }),
      test: jest.fn(() => {
        return {
          Operation: {
            enter: jest.fn((op) => calls.push(`enter operation ${op.operationId}`)),
            leave: jest.fn((op) => calls.push(`leave operation ${op.operationId}`)),
          },
        };
      }),
    };

    const document = parseYamlToDocument(
      outdent`
        openapi: 3.0.0
        paths:
          /pet:
            get:
              operationId: get
            put:
              operationId: put
      `,
      '',
    );

    await lintDocument({
      externalRefResolver: new BaseResolver(),
      document,
      config: makeConfigForRuleset(testRuleSet),
    });

    expect(calls).toMatchInlineSnapshot(`
      Array [
        "enter and skip operation get",
        "leave skipped operation get",
        "enter and not skip operation put",
        "enter operation put",
        "leave not skipped operation put",
        "leave operation put",
      ]
    `);
  });
});
