// @ts-nocheck
import {
  produce,
  applyPatches,
  immerable,
  produceWithPatches,
  enableMapSet,
  enablePatches,
  setAutoFreeze,
} from '../src/immer';

enableMapSet();
enablePatches();

describe('readme example', () => {
  it('works', () => {
    const baseState = [
      {
        todo: 'Learn typescript',
        done: true,
      },
      {
        todo: 'Try immer',
        done: false,
      },
    ];

    const nextState = produce(baseState, (draft) => {
      draft.push({ todo: 'Tweet about it' });
      draft[1].done = true;
    });

    // the new item is only added to the next state,
    // base state is unmodified
    expect(baseState.length).toBe(2);
    expect(nextState.length).toBe(3);

    // same for the changed 'done' prop
    expect(baseState[1].done).toBe(false);
    expect(nextState[1].done).toBe(true);

    // unchanged data is structurally shared
    expect(nextState[0]).toBe(baseState[0]);
    // changed data not (dûh)
    expect(nextState[1]).not.toBe(baseState[1]);
  });

  it('patches', () => {
    let state = {
      name: 'Micheal',
      age: 32,
    };

    // Let's assume the user is in a wizard, and we don't know whether
    // his changes should be updated
    let fork = state;
    // all the changes the user made in the wizard
    let changes = [];
    // all the inverse patches
    let inverseChanges = [];

    fork = produce(
      fork,
      (draft) => {
        draft.age = 33;
      },
      // The third argument to produce is a callback to which the patches will be fed
      (patches, inversePatches) => {
        changes.push(...patches);
        inverseChanges.push(...inversePatches);
      }
    );

    // In the mean time, our original state is updated as well, as changes come in from the server
    state = produce(state, (draft) => {
      draft.name = 'Michel';
    });

    // When the wizard finishes (successfully) we can replay the changes made in the fork onto the *new* state!
    state = applyPatches(state, changes);

    // state now contains the changes from both code paths!
    expect(state).toEqual({
      name: 'Michel',
      age: 33,
    });

    // Even after finishing the wizard, the user might change his mind...
    state = applyPatches(state, inverseChanges);
    expect(state).toEqual({
      name: 'Michel',
      age: 32,
    });
  });

  it('can update set', () => {
    const state = {
      title: 'hello',
      tokenSet: new Set(),
    };

    const nextState = produce(state, (draft) => {
      draft.title = draft.title.toUpperCase();
      draft.tokenSet.add('c1342');
    });

    expect(state).toEqual({ title: 'hello', tokenSet: new Set() });
    expect(nextState).toEqual({
      title: 'HELLO',
      tokenSet: new Set(['c1342']),
    });
  });

  it('can deep update map', () => {
    const state = {
      users: new Map([['michel', { name: 'miche', age: 27 }]]),
    };

    const nextState = produce(state, (draft) => {
      draft.users.get('michel').name = 'michel';
    });

    expect(state).toEqual({
      users: new Map([['michel', { name: 'miche', age: 27 }]]),
    });
    expect(nextState).toEqual({
      users: new Map([['michel', { name: 'michel', age: 27 }]]),
    });
  });

  it('supports immerable', () => {
    class Clock {
      constructor(hours = 0, minutes = 0) {
        this.hours = hours;
        this.minutes = minutes;
      }

      increment(hours, minutes = 0) {
        return produce(this, (d) => {
          d.hours += hours;
          d.minutes += minutes;
        });
      }

      toString() {
        return `${('' + this.hours).padStart(2, 0)}:${(
          '' + this.minutes
        ).padStart(2, 0)}`;
      }
    }
    Clock[immerable] = true;

    const midnight = new Clock();
    const lunch = midnight.increment(12, 30);

    expect(midnight).not.toBe(lunch);
    expect(lunch).toBeInstanceOf(Clock);
    expect(midnight.toString()).toBe('00:00');
    expect(lunch.toString()).toBe('12:30');

    const diner = lunch.increment(6);

    expect(diner).not.toBe(lunch);
    expect(lunch).toBeInstanceOf(Clock);
    expect(diner.toString()).toBe('18:30');
  });

  test('produceWithPatches', () => {
    const result = produceWithPatches(
      {
        age: 33,
      },
      (draft) => {
        draft.age++;
      }
    );
    expect(result).toEqual([
      {
        age: 34,
      },
      [
        {
          op: 'replace',
          path: ['age'],
          value: 34,
        },
      ],
      [
        {
          op: 'replace',
          path: ['age'],
          value: 33,
        },
      ],
    ]);
  });
});

test('Producers can update Maps', () => {
  setAutoFreeze(true);
  const usersById_v1 = new Map();

  const usersById_v2 = produce(usersById_v1, (draft) => {
    // Modifying a map results in a new map
    draft.set('michel', { name: 'Michel Weststrate', country: 'NL' });
  });

  const usersById_v3 = produce(usersById_v2, (draft) => {
    // Making a change deep inside a map, results in a new map as well!
    draft.get('michel').country = 'UK';
    debugger;
  });

  // We got a new map each time!
  expect(usersById_v2).not.toBe(usersById_v1);
  expect(usersById_v3).not.toBe(usersById_v2);
  // With different content obviously
  expect(usersById_v1).toMatchInlineSnapshot(`Map {}`);
  expect(usersById_v2).toMatchInlineSnapshot(`
		Map {
		  "michel" => {
		    "country": "NL",
		    "name": "Michel Weststrate",
		  },
		}
	`);
  expect(usersById_v3).toMatchInlineSnapshot(`
		Map {
		  "michel" => {
		    "country": "UK",
		    "name": "Michel Weststrate",
		  },
		}
	`);
  // The old one was never modified
  expect(usersById_v1.size).toBe(0);
  // And trying to change a Map outside a producers is going to: NO!
  expect(() => usersById_v3.clear()).toThrowErrorMatchingSnapshot();
});

test('clock class', () => {
  class Clock {
    [immerable] = true;

    constructor(hour, minute) {
      this.hour = hour;
      this.minute = minute;
    }

    get time() {
      return `${this.hour}:${this.minute}`;
    }

    tick() {
      return produce(this, (draft) => {
        draft.minute++;
      });
    }
  }

  const clock1 = new Clock(12, 10);
  const clock2 = clock1.tick();
  expect(clock1.time).toEqual('12:10'); // 12:10
  expect(clock2.time).toEqual('12:11'); // 12:11
  expect(clock2).toBeInstanceOf(Clock);
});
