/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {computed, signal} from '@angular/core';
import {FieldContext} from '../../public_api';
import {DYNAMIC} from '../../src/schema/logic';
import {LogicNodeBuilder} from '../../src/schema/logic_node';

const fakeFieldContext: FieldContext<unknown> = {
  fieldTreeOf: () => undefined!,
  stateOf: () =>
    ({
      context: undefined,
      structure: {pathKeys: () => [], parent: undefined},
    }) as any,
  valueOf: () => undefined!,
  fieldTree: undefined!,
  state: undefined!,
  value: undefined!,
  pathKeys: computed(() => []),
};

describe('LogicNodeBuilder', () => {
  it('should build logic', () => {
    // (p) => {
    //   validate(p, () => ({kind: 'root-err'}));
    // };

    const builder = LogicNodeBuilder.newRoot();
    builder.addSyncErrorRule(() => [{kind: 'root-err', fieldTree: undefined as any}]);

    const logicNode = builder.build();
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'root-err', fieldTree: undefined as any},
    ]);
  });

  it('should build child logic', () => {
    // (p) => {
    //   validate(p.a, () => ({kind: 'child-err'}));
    // };

    const builder = LogicNodeBuilder.newRoot();
    builder.getChild('a').addSyncErrorRule(() => [{kind: 'root-err', fieldTree: undefined as any}]);

    const logicNode = builder.build();
    expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'root-err', fieldTree: undefined as any},
    ]);
  });

  it('should build merged logic', () => {
    // (p) => {
    //   validate(p, () => ({kind: 'err-1'}));
    //   validate(p, () => ({kind: 'err-2'}));
    // };

    const builder = LogicNodeBuilder.newRoot();
    builder.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    const builder2 = LogicNodeBuilder.newRoot();
    builder2.addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
    builder.mergeIn(builder2);

    const logicNode = builder.build();
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
      {kind: 'err-2', fieldTree: undefined as any},
    ]);
  });

  it('should build merged child logic', () => {
    // (p) => {
    //   validate(p.a, () => ({kind: 'err-1'}));
    //   validate(p.a, () => ({kind: 'err-2'}));
    // };

    const builder = LogicNodeBuilder.newRoot();
    builder.getChild('a').addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    const builder2 = LogicNodeBuilder.newRoot();
    builder2.getChild('a').addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
    builder.mergeIn(builder2);

    const logicNode = builder.build();
    expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
      {kind: 'err-2', fieldTree: undefined as any},
    ]);
  });

  it('should build logic with predicate', () => {
    // (p) => {
    //   applyWhen(p, pred, (p) => {
    //     validate(p, () => ({kind: 'err-1'}));
    //   });
    // }

    const builder = LogicNodeBuilder.newRoot();

    const pred = signal(true);
    const builder2 = LogicNodeBuilder.newRoot();
    builder2.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
    builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});

    const logicNode = builder.build();
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);

    pred.set(false);
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
  });

  it('should apply predicate to merged in logic', () => {
    // (p) => {
    //   applyWhen(p, pred, (p) => {
    //     apply(p, (p) => {
    //       validate(p, () => ({kind: 'err-1'}));
    //     });
    //   });
    // }

    const builder = LogicNodeBuilder.newRoot();

    const pred = signal(true);
    const builder2 = LogicNodeBuilder.newRoot();

    const builder3 = LogicNodeBuilder.newRoot();
    builder3.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    builder2.mergeIn(builder3);
    builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});

    const logicNode = builder.build();
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);

    pred.set(false);
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
  });

  it('should apply predicate to merged in child logic', () => {
    // (p) => {
    //   applyWhen(p, pred, (p) => {
    //     apply(p, (p) => {
    //       validate(p.a, () => ({kind: 'err-1'}));
    //     });
    //   });
    // }

    const builder = LogicNodeBuilder.newRoot();

    const pred = signal(true);
    const builder2 = LogicNodeBuilder.newRoot();

    const builder3 = LogicNodeBuilder.newRoot();
    builder3.getChild('a').addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    builder2.mergeIn(builder3);
    builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});

    const logicNode = builder.build();
    expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);

    pred.set(false);
    expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
  });

  it('should combine predicates', () => {
    // (p) => {
    //   applyWhen(p, pred, (p) => {
    //     applyWhen(p.a, pred2, (a) => {
    //       validate(a, () => ({kind: 'err-1'}));
    //     });
    //   });
    // }

    const builder = LogicNodeBuilder.newRoot();

    const pred = signal(true);
    const builder2 = LogicNodeBuilder.newRoot();

    const pred2 = signal(true);
    const builder3 = LogicNodeBuilder.newRoot();
    builder3.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    builder2.getChild('a').mergeIn(builder3, {fn: () => pred2(), path: undefined!});
    builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});

    const logicNode = builder.build();
    expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);

    pred.set(false);
    pred2.set(true);
    expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([]);

    pred.set(true);
    pred2.set(false);
    expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
  });

  it('should propagate predicates through deep application', () => {
    // (p) => {
    //   applyWhen(p, pred, (p) => {
    //     validate(p.a.b, () => ({kind: 'err-1'}));
    //     applyWhen(p.a, pred2, (a) => {
    //       validate(a.b, () => ({kind: 'err-2'}));
    //       applyWhen(a.b, pred3, (b) => {
    //         validate(b, () => ({kind: 'err-3'}));
    //       });
    //     });
    //   });
    // }

    const builder = LogicNodeBuilder.newRoot();

    const pred = signal(true);
    const builder2 = LogicNodeBuilder.newRoot();
    builder2
      .getChild('a')
      .getChild('b')
      .addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    const pred2 = signal(true);
    const builder3 = LogicNodeBuilder.newRoot();
    builder3.getChild('b').addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);

    const pred3 = signal(true);
    const builder4 = LogicNodeBuilder.newRoot();
    builder4.addSyncErrorRule(() => [{kind: 'err-3', fieldTree: undefined as any}]);
    builder3.getChild('b').mergeIn(builder4, {fn: () => pred3(), path: undefined!});
    builder2.getChild('a').mergeIn(builder3, {fn: () => pred2(), path: undefined!});
    builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});

    const logicNode = builder.build();
    expect(
      logicNode.getChild('a').getChild('b').logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
      {kind: 'err-2', fieldTree: undefined as any},
      {kind: 'err-3', fieldTree: undefined as any},
    ]);

    pred.set(true);
    pred2.set(true);
    pred3.set(false);
    expect(
      logicNode.getChild('a').getChild('b').logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
      {kind: 'err-2', fieldTree: undefined as any},
    ]);

    pred.set(true);
    pred2.set(false);
    pred3.set(true);
    expect(
      logicNode.getChild('a').getChild('b').logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([{kind: 'err-1', fieldTree: undefined as any}]);

    pred.set(false);
    pred2.set(true);
    pred3.set(true);
    expect(
      logicNode.getChild('a').getChild('b').logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([]);
  });

  it('should propagate predicates through deep child access', () => {
    // (p) => {
    //   applyWhen(p, pred, (p) => {
    //     applyEach(p.items, (i) => {
    //       validate(i.last, () => ({kind: 'err-1'}));
    //     });
    //   });
    // };

    const builder = LogicNodeBuilder.newRoot();

    const pred = signal(true);
    const builder2 = LogicNodeBuilder.newRoot();

    const builder3 = LogicNodeBuilder.newRoot();
    builder3
      .getChild('last')
      .addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    builder2.getChild('items').getChild(DYNAMIC).mergeIn(builder3);
    builder.mergeIn(builder2, {fn: () => pred(), path: undefined!});

    const logicNode = builder.build();
    expect(
      logicNode
        .getChild('items')
        .getChild(DYNAMIC)
        .getChild('last')
        .logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([{kind: 'err-1', fieldTree: undefined as any}]);

    pred.set(false);
    expect(
      logicNode
        .getChild('items')
        .getChild(DYNAMIC)
        .getChild('last')
        .logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([]);
  });

  it('should preserve ordering across merges', () => {
    // (p) => {
    //   validate(p, () => ({kind: 'err-1'}));
    //   apply(p, (p) => {
    //     validate(p, () => ({kind: 'err-2'}));
    //   })
    //   validate(p, () => ({kind: 'err-3'}));
    // };

    const builder = LogicNodeBuilder.newRoot();
    builder.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    const builder2 = LogicNodeBuilder.newRoot();
    builder2.addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
    builder.mergeIn(builder2);

    builder.addSyncErrorRule(() => [{kind: 'err-3', fieldTree: undefined as any}]);

    const logicNode = builder.build();
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
      {kind: 'err-2', fieldTree: undefined as any},
      {kind: 'err-3', fieldTree: undefined as any},
    ]);
  });

  it('should preserve child ordering across merges', () => {
    // (p) => {
    //   validate(p.a, () => ({kind: 'err-1'}));
    //   apply(p, (p) => {
    //     validate(p.a, () => ({kind: 'err-2'}));
    //   })
    //   validate(p.a, () => ({kind: 'err-3'}));
    // };

    const builder = LogicNodeBuilder.newRoot();
    builder.getChild('a').addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);

    const builder2 = LogicNodeBuilder.newRoot();
    builder2.getChild('a').addSyncErrorRule(() => [{kind: 'err-2', fieldTree: undefined as any}]);
    builder.mergeIn(builder2);

    builder.getChild('a').addSyncErrorRule(() => [{kind: 'err-3', fieldTree: undefined as any}]);

    const logicNode = builder.build();
    expect(logicNode.getChild('a').logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
      {kind: 'err-2', fieldTree: undefined as any},
      {kind: 'err-3', fieldTree: undefined as any},
    ]);
  });

  it('should support circular logic structures', () => {
    // const s = schema((p) => {
    //   validate(p, () => ({kind: 'err-1'})),
    //   apply(p.next, s);
    // }));

    const builder = LogicNodeBuilder.newRoot();
    builder.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
    builder.getChild('next').mergeIn(builder);

    const logicNode = builder.build();
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);
    expect(logicNode.getChild('next').logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);
    expect(
      logicNode.getChild('next').getChild('next').logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([{kind: 'err-1', fieldTree: undefined as any}]);
  });

  it('should support circular logic structures with predicate', () => {
    // const s = schema((p) => {
    //   validate(p, () => ({kind: 'err-1'})),
    //   applyWhen(p.next, pred, s);
    // }));

    const pred = signal(true);
    const builder = LogicNodeBuilder.newRoot();
    builder.addSyncErrorRule(() => [{kind: 'err-1', fieldTree: undefined as any}]);
    builder.getChild('next').mergeIn(builder, {fn: () => pred(), path: undefined!});

    const logicNode = builder.build();
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);
    expect(logicNode.getChild('next').logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);
    expect(
      logicNode.getChild('next').getChild('next').logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([{kind: 'err-1', fieldTree: undefined as any}]);

    // TODO: test that verifies that the same predicate can resolve with a different field context
    // on `.next` vs on `.next.next`
    pred.set(false);
    expect(logicNode.logic.syncErrors.compute(fakeFieldContext)).toEqual([
      {kind: 'err-1', fieldTree: undefined as any},
    ]);
    expect(logicNode.getChild('next').logic.syncErrors.compute(fakeFieldContext)).toEqual([]);
    expect(
      logicNode.getChild('next').getChild('next').logic.syncErrors.compute(fakeFieldContext),
    ).toEqual([]);
  });
});
