import VCS from '../index';
import MemoryDriver from '../../store/drivers/memory-driver';
import { describeChanges } from '../util';

function newDoc(id) {
  return { id };
}

async function vcs(branch) {
  const v = new VCS(new MemoryDriver());
  await v.switchAndCreateProjectIfNotExist('workspace_1', 'Test Workspace');
  await v.checkout([], branch);
  return v;
}

describe('VCS', () => {
  beforeEach(() => {
    let ts = 1000000000000;
    Date.now = jest.fn(() => ts++);
  });

  describe('status()', () => {
    it('returns status with no commits', async () => {
      const v = await vcs('master');
      const status = await v.status(
        [
          {
            key: 'foo',
            name: 'Foo',
            document: newDoc('bar'),
          },
          {
            key: 'baz',
            name: 'Baz',
            document: newDoc('qux'),
          },
        ],
        {},
      );

      expect(status).toEqual({
        key: '0cffac636df909fb4f8e9a25570e5012846b46fd',
        stage: {},
        unstaged: {
          foo: {
            added: true,
            key: 'foo',
            blobContent: '{"id":"bar"}',
            blobId: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
            name: 'Foo',
          },
          baz: {
            added: true,
            key: 'baz',
            blobContent: '{"id":"qux"}',
            blobId: 'dc61fb5cb183286293dc7f0e8499b4e1e09eef05',
            name: 'Baz',
          },
        },
      });
    });

    it('returns add/modify/delete operations', async () => {
      const v = await vcs('master');

      const status1 = await v.status(
        [
          { key: 'a', name: 'A', document: newDoc('aaa') },
          { key: 'b', name: 'B', document: newDoc('bbb') },
          { key: 'c', name: 'C', document: newDoc('ccc') },
        ],
        {},
      );
      expect(Object.keys(status1.unstaged)).toEqual(['a', 'b', 'c']);

      await v.stage(status1.stage, [
        status1.unstaged['a'],
        status1.unstaged['b'],
        status1.unstaged['c'],
      ]);

      await v.takeSnapshot(status1.stage, 'Add a/b/c');
      const history = await v.getHistory();
      expect(history.length).toBe(1);
      expect(history).toEqual([
        {
          created: expect.any(Date),
          description: '',
          author: '',
          id: 'a5fb7270c060c0387967c6db725fdf07e9594911',
          name: 'Add a/b/c',
          parent: '0000000000000000000000000000000000000000',
          state: [
            {
              blob: '210be55f7998bb55805f9d65c5345103e7957929',
              key: 'a',
              name: 'A',
            },
            {
              blob: '8ce1aded8fa999d1c5632ff993b56dd9aa1f4880',
              key: 'b',
              name: 'B',
            },
            {
              blob: 'ce29c03e80866a215004d55f160a9a7b510ceacb',
              key: 'c',
              name: 'C',
            },
          ],
        },
      ]);

      // Should get every operation type
      const status = await v.status(
        [
          { key: 'notA', name: 'Not A', document: newDoc('aaa') },
          { key: 'b', name: 'B', document: newDoc('bbb') },
          { key: 'c', name: 'C', document: newDoc('modified') },
          { key: 'd', name: 'D', document: newDoc('ddd') },
        ],
        {},
      );

      expect(status).toEqual({
        key: '80834d815272c73192291477f70dce8a7c8bc424',
        stage: {},
        unstaged: {
          a: {
            deleted: true,
            blobId: '210be55f7998bb55805f9d65c5345103e7957929',
            key: 'a',
            name: 'A',
          },
          notA: {
            added: true,
            key: 'notA',
            name: 'Not A',
            blobId: '210be55f7998bb55805f9d65c5345103e7957929',
            blobContent: '{"id":"aaa"}',
          },
          c: {
            modified: true,
            key: 'c',
            name: 'C',
            blobId: 'c32090250612936632fa4a3127507aa0694ba375',
            blobContent: '{"id":"modified"}',
          },
          d: {
            added: true,
            key: 'd',
            name: 'D',
            blobId: 'd4d10ce251d7184fa14587796aed6e06c549f7a4',
            blobContent: '{"id":"ddd"}',
          },
        },
      });

      const newStage = await v.stage(status.stage, [
        status.unstaged['a'],
        status.unstaged['notA'],
        status.unstaged['c'],
        status.unstaged['d'],
      ]);

      const status2 = await v.status(
        [
          { key: 'notA', name: 'Not A', document: newDoc('aaa') },
          { key: 'b', name: 'B', document: newDoc('bbb') },
          { key: 'c', name: 'C', document: newDoc('modified') },
          { key: 'd', name: 'D', document: newDoc('ddd') },
        ],
        newStage,
      );

      expect(status2).toEqual({
        key: '4a0e8fd9ee30acb9c8ff4eb2f73e413ff7175a8c',
        stage: {
          a: {
            deleted: true,
            blobId: '210be55f7998bb55805f9d65c5345103e7957929',
            key: 'a',
            name: 'A',
          },
          notA: {
            added: true,
            blobId: '210be55f7998bb55805f9d65c5345103e7957929',
            key: 'notA',
            name: 'Not A',
            blobContent: '{"id":"aaa"}',
          },
          c: {
            modified: true,
            blobId: 'c32090250612936632fa4a3127507aa0694ba375',
            key: 'c',
            name: 'C',
            blobContent: '{"id":"modified"}',
          },
          d: {
            added: true,
            blobId: 'd4d10ce251d7184fa14587796aed6e06c549f7a4',
            key: 'd',
            name: 'D',
            blobContent: '{"id":"ddd"}',
          },
        },
        unstaged: {},
      });
    });

    it('can appear both staged and unstaged', async () => {
      const v = await vcs('master');

      const status = await v.status(
        [
          { key: 'a', name: 'A', document: newDoc('aaa') },
          { key: 'b', name: 'B', document: newDoc('bbb') },
        ],
        {},
      );
      const stage = await v.stage(status.stage, [status.unstaged['a']]);

      const status2 = await v.status(
        [
          { key: 'a', name: 'A', document: newDoc('modified') },
          { key: 'b', name: 'B', document: newDoc('bbb') },
        ],
        stage,
      );

      expect(status2).toEqual({
        key: '886337817e3c1ff7df3fcfa7c30192eb854b45cf',
        stage: {
          a: {
            added: true,
            blobId: '210be55f7998bb55805f9d65c5345103e7957929',
            name: 'A',
            key: 'a',
            blobContent: '{"id":"aaa"}',
          },
        },
        unstaged: {
          a: {
            added: true,
            blobId: 'c32090250612936632fa4a3127507aa0694ba375',
            key: 'a',
            name: 'A',
            blobContent: '{"id":"modified"}',
          },
          b: {
            added: true,
            blobId: '8ce1aded8fa999d1c5632ff993b56dd9aa1f4880',
            name: 'B',
            key: 'b',
            blobContent: '{"id":"bbb"}',
          },
        },
      });
    });

    it('should not show committed entities', async () => {
      const v = await vcs('master');

      const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
      const stage2 = await v.stage(status.stage, [status.unstaged['foo']]);
      await v.takeSnapshot(stage2, 'Add foo');

      const status2 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
      expect(status2).toEqual({
        key: 'ca455b43c1e992812d81032e79e037a8db85fa8b',
        stage: {},
        unstaged: {},
      });
    });
  });

  describe('stage()', () => {
    it('stages entity', async () => {
      const v = await vcs('master');

      const status = await v.status(
        [
          { key: 'foo', name: 'Foo', document: newDoc('bar') },
          { key: 'baz', name: 'Baz', document: newDoc('qux') },
        ],
        {},
      );

      const stage = await v.stage(status.stage, [status.unstaged['foo']]);
      expect(stage).toEqual({
        foo: {
          key: 'foo',
          name: 'Foo',
          blobContent: '{"id":"bar"}',
          blobId: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
          added: true,
        },
      });

      const status2 = await v.status(
        [
          { key: 'foo', name: 'Foo', document: newDoc('bar') },
          { key: 'baz', name: 'Baz', document: newDoc('qux') },
        ],
        stage,
      );
      expect(status2).toEqual({
        key: '19c281e6bbbebec0143491bf14f9a013a4114125',
        stage: {
          foo: {
            name: 'Foo',
            key: 'foo',
            blobContent: '{"id":"bar"}',
            blobId: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
            added: true,
          },
        },
        unstaged: {
          baz: {
            key: 'baz',
            name: 'Baz',
            blobContent: '{"id":"qux"}',
            blobId: 'dc61fb5cb183286293dc7f0e8499b4e1e09eef05',
            added: true,
          },
        },
      });
    });
  });

  describe('takeSnapshot()', () => {
    it('commits basic entity', async () => {
      const v = await vcs('master');

      const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
      const stage = await v.stage(status.stage, [status.unstaged['foo']]);
      await v.takeSnapshot(stage, 'Add foo');

      const history = await v.getHistory();
      expect(history).toEqual([
        {
          id: '24b4888f4cecc5a1ac272eb1641cb5aa102c1675',
          author: '',
          created: expect.any(Date),
          name: 'Add foo',
          description: '',
          parent: '0000000000000000000000000000000000000000',
          state: [
            {
              blob: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
              key: 'foo',
              name: 'Foo',
            },
          ],
        },
      ]);
    });

    it('commits deleted entity', async () => {
      const v = await vcs('master');

      const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
      const stage = await v.stage(status.stage, [status.unstaged['foo']]);
      await v.takeSnapshot(stage, 'Add foo');

      const history = await v.getHistory();
      expect(history).toEqual([
        {
          id: '365c4341f6d57e18994df2429387b78f2c9a31cb',
          author: '',
          created: expect.any(Date),
          name: 'Add foo',
          description: '',
          parent: '0000000000000000000000000000000000000000',
          state: [
            {
              blob: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
              key: 'foo',
              name: 'Foo',
            },
          ],
        },
      ]);

      const status2 = await v.status([], {});
      const stage2 = await v.stage(status2.stage, [status2.unstaged['foo']]);
      await v.takeSnapshot(stage2, 'Delete foo');
      const history2 = await v.getHistory();

      expect(history2).toEqual([
        {
          id: '365c4341f6d57e18994df2429387b78f2c9a31cb',
          author: '',
          created: expect.any(Date),
          name: 'Add foo',
          description: '',
          parent: '0000000000000000000000000000000000000000',
          state: [
            {
              blob: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
              key: 'foo',
              name: 'Foo',
            },
          ],
        },
        {
          id: '42f5b05d004f99e875a7eb3fe24ad7f94ad3e0c5',
          author: '',
          created: expect.any(Date),
          name: 'Delete foo',
          description: '',
          parent: '365c4341f6d57e18994df2429387b78f2c9a31cb',
          state: [],
        },
      ]);
    });
  });

  describe('getBranches()', () => {
    it('lists branches', async () => {
      const v = await vcs('master');

      await v.checkout([], 'branch-1');
      await v.checkout([], 'branch-2');

      const branches = await v.getBranches();
      expect(branches).toEqual(['master', 'branch-1', 'branch-2']);
    });
  });

  describe('removeBranch()', () => {
    it('cannot remove empty branch', async () => {
      const v = await vcs('master');

      let didError = false;
      try {
        await v.removeBranch();
      } catch (err) {
        didError = true;
      }

      expect(didError).toBe(true);
    });

    it('cannot remove current branch', async () => {
      const v = await vcs('master');

      let didError = false;
      try {
        await v.removeBranch('master');
      } catch (err) {
        didError = true;
      }

      expect(didError).toBe(true);
    });

    it('remove branch', async () => {
      const v = await vcs('master');

      // Add something to master
      const status1 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
      const stage1 = await v.stage(status1.stage, [status1.unstaged['foo']]);
      await v.takeSnapshot(stage1, 'Add foo');

      // Checkout branch
      await v.checkout([], 'new-branch');
      expect(await v.getBranches()).toEqual(['master', 'new-branch']);

      // Back to master and delete other branch
      await v.checkout([], 'master');
      await v.removeBranch('new-branch');
      expect(await v.getBranches()).toEqual(['master']);
    });
  });

  describe('fork()', () => {
    it('forks to a new branch', async () => {
      const v = await vcs('master');

      // Add something to master
      const status1 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
      const stage1 = await v.stage(status1.stage, [status1.unstaged['foo']]);
      await v.takeSnapshot(stage1, 'Add foo');

      // Checkout branch
      await v.fork('new-branch');
      await v.checkout([], 'new-branch');
      const history = await v.getHistory();
      expect(await v.getBranch()).toBe('new-branch');
      expect(history).toEqual([
        {
          created: expect.any(Date),
          id: 'e1afd0f5607d8da06b4360eb22d41650262f6dc5',
          author: '',
          parent: '0000000000000000000000000000000000000000',
          name: 'Add foo',
          description: '',
          state: [
            {
              blob: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
              key: 'foo',
              name: 'Foo',
            },
          ],
        },
      ]);
    });
  });

  describe('merge()', () => {
    it('performs fast-forward merge', async () => {
      const v = await vcs('master');

      const status1 = await v.status(
        [
          { key: 'a', name: 'A', document: newDoc('aaa') },
          { key: 'b', name: 'B', document: newDoc('bbb') },
        ],
        {},
      );
      const stage1 = await v.stage(status1.stage, [status1.unstaged['a'], status1.unstaged['b']]);
      await v.takeSnapshot(stage1, 'Add A and B');
      expect((await v.getHistory())[0].state).toEqual([
        expect.objectContaining({ key: 'a' }),
        expect.objectContaining({ key: 'b' }),
      ]);

      await v.fork('feature-a');
      await v.checkout([], 'feature-a');
      const status2 = await v.status(
        [
          { key: 'a', name: 'A', document: newDoc('aaa') },
          { key: 'b', name: 'B', document: newDoc('bbbbbbb') },
          { key: 'c', name: 'C', document: newDoc('ccc') },
        ],
        status1.stage,
      );
      const stage2 = await v.stage(status2.stage, [status2.unstaged['b'], status2.unstaged['c']]);
      await v.takeSnapshot(stage2, 'Add C, modify B');
      expect((await v.getHistory())[1].state).toEqual(
        expect.arrayContaining([
          expect.objectContaining({ key: 'a' }),
          expect.objectContaining({ key: 'b' }),
          expect.objectContaining({ key: 'c' }),
        ]),
      );
    });

    it('merges even if no common root', async () => {
      const v = await vcs('master');

      const status1 = await v.status(
        [
          { key: 'a', name: 'A', document: newDoc('aaa') },
          { key: 'b', name: 'B', document: newDoc('bbb') },
        ],
        {},
      );
      const stage1 = await v.stage(status1.stage, [status1.unstaged['a'], status1.unstaged['b']]);
      await v.takeSnapshot(stage1, 'message');
    });

    it('does something', async () => {
      const v = await vcs('master');

      // Add a file to master
      expect(await v.getBranch()).toBe('master');
      const status1 = await v.status([{ key: 'a', name: 'A', document: newDoc('aaa') }], {});
      const stage = await v.stage(status1.stage, [status1.unstaged['a']]);
      await v.takeSnapshot(stage, 'Add A');
      expect(await v.getHistory()).toEqual([
        {
          id: 'c0b75a0cb27c946e376326b963ce544af08b680a',
          author: '',
          created: expect.any(Date),
          parent: '0000000000000000000000000000000000000000',
          name: 'Add A',
          description: '',
          state: [
            {
              blob: '210be55f7998bb55805f9d65c5345103e7957929',
              key: 'a',
              name: 'A',
            },
          ],
        },
      ]);

      // Checkout new branch and add file
      await v.fork('new-branch');
      await v.checkout([], 'new-branch');
      expect(await v.getBranch()).toBe('new-branch');

      const status2 = await v.status([{ key: 'b', name: 'B', document: newDoc('bbb') }], {});
      const stage2 = await v.stage(status2.stage, [status2.unstaged['b']]);
      await v.takeSnapshot(stage2, 'Add B');
      expect(await v.getHistory()).toEqual([
        {
          id: 'c0b75a0cb27c946e376326b963ce544af08b680a',
          author: '',
          created: expect.any(Date),
          parent: '0000000000000000000000000000000000000000',
          name: 'Add A',
          description: '',
          state: [
            {
              blob: '210be55f7998bb55805f9d65c5345103e7957929',
              key: 'a',
              name: 'A',
            },
          ],
        },
        {
          id: 'b0fb09c8ac80b34f94dc6751da6b5aa634682f45',
          author: '',
          created: expect.any(Date),
          parent: 'c0b75a0cb27c946e376326b963ce544af08b680a',
          name: 'Add B',
          description: '',
          state: [
            {
              blob: '210be55f7998bb55805f9d65c5345103e7957929',
              key: 'a',
              name: 'A',
            },
            {
              blob: '8ce1aded8fa999d1c5632ff993b56dd9aa1f4880',
              key: 'b',
              name: 'B',
            },
          ],
        },
      ]);

      // Merge new branch back into master
      await v.checkout([], 'master');
      expect(await v.getBranch()).toBe('master');
      await v.merge([], 'new-branch');
      expect(await v.getHistory()).toEqual([
        {
          id: 'c0b75a0cb27c946e376326b963ce544af08b680a',
          author: '',
          created: expect.any(Date),
          parent: '0000000000000000000000000000000000000000',
          name: 'Add A',
          description: '',
          state: [
            {
              blob: '210be55f7998bb55805f9d65c5345103e7957929',
              key: 'a',
              name: 'A',
            },
          ],
        },
        {
          id: 'b0fb09c8ac80b34f94dc6751da6b5aa634682f45',
          author: '',
          created: expect.any(Date),
          parent: 'c0b75a0cb27c946e376326b963ce544af08b680a',
          name: 'Add B',
          description: '',
          state: [
            {
              blob: '210be55f7998bb55805f9d65c5345103e7957929',
              key: 'a',
              name: 'A',
            },
            {
              blob: '8ce1aded8fa999d1c5632ff993b56dd9aa1f4880',
              key: 'b',
              name: 'B',
            },
          ],
        },
      ]);
    });
  });
  describe('describeChanges()', () => {
    it('works with same object structure', async () => {
      const a = { foo: 'bar', nested: { baz: 10 } };
      const b = { foo: 'baz', nested: { baz: 11 } };
      expect(describeChanges(a, b)).toEqual(['foo', 'nested']);
    });

    it('ignores modified key', () => {
      const a = { foo: 'bar', nested: { baz: 10 }, modified: 10 };
      const b = { foo: 'baz', nested: { baz: 11 }, modified: 12 };
      expect(describeChanges(a, b)).toEqual(['foo', 'nested']);
    });

    it('skips invalid values', () => {
      const a = null;
      const b = { foo: 'baz', nested: { baz: 11 }, modified: 12 };
      expect(describeChanges(a, b)).toEqual([]);
    });
  });
});
