import { describe, it } from 'node:test';

import { expect } from 'chai';

import { buildSchema } from '../buildASTSchema.ts';
import {
  BreakingChangeType,
  DangerousChangeType,
  findSchemaChanges,
  SafeChangeType,
} from '../findSchemaChanges.ts';

describe('findSchemaChanges', () => {
  it('should detect if a type was added', () => {
    const newSchema = buildSchema(`
      type Type1
      type Type2
    `);

    const oldSchema = buildSchema(`
      type Type1
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        type: SafeChangeType.TYPE_ADDED,
        description: 'Type2 was added.',
      },
    ]);
  });

  it('should detect a type changing description', () => {
    const newSchema = buildSchema(`
      "New Description"
      type Type1
    `);

    const oldSchema = buildSchema(`
      type Type1
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        type: SafeChangeType.DESCRIPTION_CHANGED,
        description: 'Description of Type1 has changed to "New Description".',
      },
    ]);
  });

  it('should detect if a field was added', () => {
    const oldSchema = buildSchema(`
      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      type Query {
        foo: String
        bar: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        type: SafeChangeType.FIELD_ADDED,
        description: 'Field Query.bar was added.',
      },
    ]);
  });

  it('should detect a field changing description', () => {
    const oldSchema = buildSchema(`
      type Query {
        foo: String
        bar: String
      }
    `);

    const newSchema = buildSchema(`
      type Query {
        foo: String
        "New Description"
        bar: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        type: SafeChangeType.DESCRIPTION_CHANGED,
        description:
          'Description of field Query.bar has changed to "New Description".',
      },
    ]);
  });

  it('should detect if a default value was added', () => {
    const oldSchema = buildSchema(`
      type Query {
        foo(x: String): String
      }
    `);

    const newSchema = buildSchema(`
      type Query {
        foo(x: String = "bar"): String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        type: SafeChangeType.ARG_DEFAULT_VALUE_ADDED,
        description: 'Query.foo(x:) added a defaultValue "bar".',
      },
    ]);
  });

  it('should detect if an arg value changes safely', () => {
    const oldSchema = buildSchema(`
      type Query {
        foo(x: String!): String
      }
    `);

    const newSchema = buildSchema(`
      type Query {
        foo(x: String): String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        type: SafeChangeType.ARG_CHANGED_KIND_SAFE,
        description:
          'Argument Query.foo(x:) has changed type from String! to String.',
      },
    ]);
  });

  it('should detect if an arg value changes description', () => {
    const oldSchema = buildSchema(`
      type Query {
        foo(x: String!): String
      }
    `);

    const newSchema = buildSchema(`
      type Query {
        foo(
          "New Description"
          x: String!
        ): String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        type: SafeChangeType.DESCRIPTION_CHANGED,
        description:
          'Description of argument Query.foo(x:) has changed to "New Description".',
      },
    ]);
  });

  it('should detect if a directive was added', () => {
    const oldSchema = buildSchema(`
      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description: 'Directive @Foo was added.',
        type: SafeChangeType.DIRECTIVE_ADDED,
      },
    ]);
  });

  it('should detect if a changes argument safely', () => {
    const oldSchema = buildSchema(`
      directive @Foo(foo: String!) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo(foo: String) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description:
          'Argument @Foo(foo:) has changed type from String! to String.',
        type: SafeChangeType.ARG_CHANGED_KIND_SAFE,
      },
    ]);
  });

  it('should detect if a default value is added to an argument', () => {
    const oldSchema = buildSchema(`
      directive @Foo(foo: String) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo(foo: String = "Foo") on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description: '@Foo(foo:) added a defaultValue "Foo".',
        type: SafeChangeType.ARG_DEFAULT_VALUE_ADDED,
      },
    ]);
  });

  it('should detect if a default value is removed from an argument', () => {
    const newSchema = buildSchema(`
      directive @Foo(foo: String) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const oldSchema = buildSchema(`
      directive @Foo(foo: String = "Foo") on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description: '@Foo(foo:) defaultValue was removed.',
        type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
      },
    ]);
  });

  it('should detect if a default value is changed in an argument', () => {
    const oldSchema = buildSchema(`
      directive @Foo(foo: String = "Bar") on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo(foo: String = "Foo") on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description: '@Foo(foo:) has changed defaultValue from "Bar" to "Foo".',
        type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE,
      },
    ]);
  });

  it('should detect if a directive argument does a breaking change', () => {
    const oldSchema = buildSchema(`
      directive @Foo(foo: String) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo(foo: String!) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description:
          'Argument @Foo(foo:) has changed type from String to String!.',
        type: BreakingChangeType.ARG_CHANGED_KIND,
      },
    ]);
  });

  it('should not detect if a directive argument default value does not change', () => {
    const oldSchema = buildSchema(`
      directive @Foo(foo: String = "FOO") on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo(foo: String = "FOO") on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([]);
  });

  it('should detect if a directive changes description', () => {
    const oldSchema = buildSchema(`
      directive @Foo on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      "New Description"
      directive @Foo on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description: 'Description of @Foo has changed to "New Description".',
        type: SafeChangeType.DESCRIPTION_CHANGED,
      },
    ]);
  });

  it('should detect if a directive becomes repeatable', () => {
    const oldSchema = buildSchema(`
      directive @Foo on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo repeatable on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description: 'Repeatable flag was added to @Foo.',
        type: SafeChangeType.DIRECTIVE_REPEATABLE_ADDED,
      },
    ]);
  });

  it('should detect if a directive adds locations', () => {
    const oldSchema = buildSchema(`
      directive @Foo on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo on FIELD_DEFINITION | QUERY

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description: 'QUERY was added to @Foo.',
        type: SafeChangeType.DIRECTIVE_LOCATION_ADDED,
      },
    ]);
  });

  it('should detect if a directive arg gets added', () => {
    const oldSchema = buildSchema(`
      directive @Foo on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo(arg1: String) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description: 'An optional argument @Foo(arg1:) was added.',
        type: SafeChangeType.OPTIONAL_DIRECTIVE_ARG_ADDED,
      },
    ]);
  });

  it('should detect if a directive arg changes description', () => {
    const oldSchema = buildSchema(`
      directive @Foo(
        arg1: String
      ) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      directive @Foo(
        "New Description"
        arg1: String
      ) on FIELD_DEFINITION

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description:
          'Description of @Foo(Foo) has changed to "New Description".',
        type: SafeChangeType.DESCRIPTION_CHANGED,
      },
    ]);
  });

  it('should detect if an enum member changes description', () => {
    const oldSchema = buildSchema(`
      enum Foo {
        TEST
      }

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      enum Foo {
        "New Description"
        TEST
      }

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description:
          'Description of enum value Foo.TEST has changed to "New Description".',
        type: SafeChangeType.DESCRIPTION_CHANGED,
      },
    ]);
  });

  it('should detect if an input field changes description', () => {
    const oldSchema = buildSchema(`
      input Foo {
        x: String
      }

      type Query {
        foo: String
      }
    `);

    const newSchema = buildSchema(`
      input Foo {
        "New Description"
        x: String
      }

      type Query {
        foo: String
      }
    `);
    expect(findSchemaChanges(oldSchema, newSchema)).to.deep.equal([
      {
        description:
          'Description of input-field Foo.x has changed to "New Description".',
        type: SafeChangeType.DESCRIPTION_CHANGED,
      },
    ]);
  });
});