import crypto from 'crypto';
import fs from 'fs';
import { Uint64BE } from 'int64-buffer';

import * as repertory from '../index.js';
import connection from '../networking/connection';
import connection_pool from '../networking/connection_pool';

const TEST_HOST = process.env.TEST_HOST || 'localhost';
const TEST_PASSWORD = process.env.TEST_PASSWORD || '';
const TEST_PORT = process.env.TEST_PORT || 20000;

const calculate_sha256 = (path) => {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash('sha256');

    fs.createReadStream(path)
      .on('data', (data) => hash.update(data))
      .on('error', (err) => reject(err))
      .on('end', () => {
        const h = hash.digest('hex');
        console.log(path, h);
        resolve(h);
      });
  });
};

const test_connection = (conn, should_be_connected) => {
  expect(conn).toBeInstanceOf(connection);
  expect(conn.host_or_ip).toEqual(TEST_HOST);
  expect(conn.port).toEqual(TEST_PORT);
  expect(conn.password).toEqual(TEST_PASSWORD);
  expect(conn.connected).toEqual(should_be_connected);
  console.log(conn);
};

test('can create a connection to repertory api', async () => {
  const conn = await repertory.connect(TEST_HOST, TEST_PORT, TEST_PASSWORD);
  test_connection(conn, true);

  await conn.disconnect();
});

test('create_pool returns a connection if pool size is <=1', async () => {
  for (let i = 0; i < 2; i++) {
    const conn = await repertory.create_pool(
      i,
      TEST_HOST,
      TEST_PORT,
      TEST_PASSWORD
    );
    expect(conn).toBeInstanceOf(connection);
    test_connection(conn, true);

    await conn.disconnect();
  }
});

test('can create a connection pool', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  console.log(conn);
  expect(conn).toBeInstanceOf(connection_pool);
  expect(conn.host_or_ip).toEqual(TEST_HOST);
  expect(conn.port).toEqual(TEST_PORT);
  expect(conn.password).toEqual(TEST_PASSWORD);
  expect(conn.shutdown).toEqual(false);
  expect(conn.pool._pool.max).toEqual(2);
  expect(conn.pool._pool.min).toEqual(2);

  await conn.disconnect();
});

test('can get drive information using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  const di = await api.get_drive_information();
  console.log(di);

  expect(di.free).toBeDefined();
  expect(di.total).toBeDefined();
  expect(di.used).toBeDefined();

  await conn.disconnect();
});

test('can create and remove a directory using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(await api.directory.create('/repertory_js')).toEqual(0);
  expect(await api.directory.exists('/repertory_js')).toEqual(true);
  expect(await api.file.exists('/repertory_js')).toEqual(false);
  expect(await api.directory.remove('/repertory_js')).toEqual(0);

  await conn.disconnect();
});

test('can get directory list and snapshot using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);

  const test_results = async (remote_path, page_count, get_page) => {
    expect(remote_path).toEqual('/');
    expect(page_count).toBeGreaterThanOrEqual(1);
    expect(get_page).toBeInstanceOf(Function);
    for (let i = 0; i < page_count; i++) {
      const items = await get_page(i);
      console.log(items);

      expect(items.length).toBeGreaterThanOrEqual(2);
      expect(items[0].directory).toBeTruthy();
      expect(items[0].path).toEqual('.');
      expect(items[1].directory).toBeTruthy();
      expect(items[1].path).toEqual('..');
    }
  };

  await api.directory.list('/', async (remote_path, page_count, get_page) => {
    console.log(remote_path, page_count, get_page);
    await test_results(remote_path, page_count, get_page);
  });

  const snap = await api.directory.snapshot('/');
  try {
    console.log(snap.remote_path, snap.page_count, snap.get_page);
    await test_results(snap.remote_path, snap.page_count, snap.get_page);
  } catch (err) {
    console.log(err);
  } finally {
    await snap.release();
  }

  await conn.disconnect();
});

test('can create, close and delete a file using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  const f = await api.file.create_or_open('/repertory_file.dat');
  console.log(f);
  expect(f.remote_path).toEqual('/repertory_file.dat');
  expect(f.conn).toEqual(conn);
  expect(new Uint64BE(f.handle).toNumber()).toBeGreaterThanOrEqual(0);

  expect(await f.close()).toEqual(0);
  expect(f.handle).toBeNull();

  expect(await api.file.delete('/repertory_file.dat')).toEqual(0);

  await conn.disconnect();
});

test('can open, close and delete a file using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  let f = await api.file.create_or_open('/repertory_file.dat');
  expect(await f.close()).toEqual(0);

  f = await api.file.open('/repertory_file.dat');
  console.log(f);
  expect(f.remote_path).toEqual('/repertory_file.dat');
  expect(f.conn).toEqual(conn);
  expect(new Uint64BE(f.handle).toNumber()).toBeGreaterThanOrEqual(0);

  expect(await f.close()).toEqual(0);
  expect(f.handle).toBeNull();

  expect(await api.file.delete('/repertory_file.dat')).toEqual(0);

  await conn.disconnect();
});

test('can write to and read from a file using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  const f = await api.file.create_or_open('/repertory_file.dat');

  const buffer = Buffer.alloc(4);
  buffer[0] = 1;
  buffer[1] = 2;
  buffer[2] = 3;
  buffer[3] = 4;
  expect(await f.write(0, buffer)).toEqual(buffer.length);
  expect(new Uint64BE(await f.get_size()).toNumber()).toEqual(buffer.length);

  const buffer2 = await f.read(0, 4);
  expect(buffer.compare(buffer2)).toEqual(0);

  expect(await f.close()).toEqual(0);
  expect(await api.file.delete('/repertory_file.dat')).toEqual(0);

  await conn.disconnect();
});

test('can truncate a file using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  const f = await api.file.create_or_open('/repertory_file.dat');

  expect(await f.truncate(10)).toEqual(0);
  expect(new Uint64BE(await f.get_size()).toNumber()).toEqual(10);

  expect(await f.truncate(0)).toEqual(0);
  expect(new Uint64BE(await f.get_size()).toNumber()).toEqual(0);

  expect(await f.close()).toEqual(0);
  expect(await api.file.delete('/repertory_file.dat')).toEqual(0);

  await conn.disconnect();
});

test('can upload and download a file using api', async () => {
  try {
    fs.unlinkSync('repertory_test.dat');
  } catch {}

  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(
    await api.file.upload('test.dat', '/repertory_test.dat', (l, r, p, c) => {
      console.log(l, r, p, c);
    })
  ).toBeTruthy();

  expect(
    await api.file.download(
      '/repertory_test.dat',
      'repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      }
    )
  ).toBeTruthy();

  expect(await calculate_sha256('test.dat')).toEqual(
    await calculate_sha256('repertory_test.dat')
  );

  expect(await api.directory.exists('/repertory_test.dat')).toEqual(false);
  expect(await api.file.exists('/repertory_test.dat')).toEqual(true);
  expect(await api.file.delete('/repertory_test.dat')).toEqual(0);
  fs.unlinkSync('repertory_test.dat');

  await conn.disconnect();
}, 60000);

test('can download and overwrite a file using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(
    await api.file.upload('test.dat', '/repertory_test.dat', (l, r, p, c) => {
      console.log(l, r, p, c);
    })
  ).toBeTruthy();

  expect(
    await api.file.download(
      '/repertory_test.dat',
      'repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      }
    )
  ).toBeTruthy();

  expect(
    await api.file.download(
      '/repertory_test.dat',
      'repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      },
      true
    )
  ).toBeTruthy();

  expect(await api.file.delete('/repertory_test.dat')).toEqual(0);
  fs.unlinkSync('repertory_test.dat');

  await conn.disconnect();
}, 60000);

test('download fails if overwrite is false using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(
    await api.file.upload('test.dat', '/repertory_test.dat', (l, r, p, c) => {
      console.log(l, r, p, c);
    })
  ).toBeTruthy();

  expect(
    await api.file.download(
      '/repertory_test.dat',
      'repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      }
    )
  ).toBeTruthy();

  await expect(
    api.file.download(
      '/repertory_test.dat',
      'repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      },
      false
    )
  ).rejects.toThrow(Error);

  expect(await api.file.delete('/repertory_test.dat')).toEqual(0);
  fs.unlinkSync('repertory_test.dat');

  await conn.disconnect();
}, 60000);

test('can upload and overwrite a file using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(
    await api.file.upload('test.dat', '/repertory_test.dat', (l, r, p, c) => {
      console.log(l, r, p, c);
    })
  ).toBeTruthy();

  expect(
    await api.file.upload(
      'test.dat',
      '/repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      },
      true
    )
  ).toBeTruthy();

  expect(await api.file.delete('/repertory_test.dat')).toEqual(0);

  await conn.disconnect();
}, 60000);

test('upload fails if overwrite is false using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(
    await api.file.upload('test.dat', '/repertory_test.dat', (l, r, p, c) => {
      console.log(l, r, p, c);
    })
  ).toBeTruthy();

  await expect(
    api.file.upload(
      'test.dat',
      '/repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      },
      false
    )
  ).rejects.toThrow(Error);

  expect(await api.file.delete('/repertory_test.dat')).toEqual(0);

  await conn.disconnect();
}, 60000);

test('can resume download using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(
    await api.file.upload('test.dat', '/repertory_test.dat', (l, r, p, c) => {
      console.log(l, r, p, c);
    })
  ).toBeTruthy();

  const fd = fs.openSync('test.dat', 'r');
  const buffer = Buffer.alloc(1024);
  fs.readSync(fd, buffer, 0, buffer.length);
  fs.closeSync(fd);

  fs.writeFileSync('repertory_test.dat', buffer);

  expect(
    await api.file.download(
      '/repertory_test.dat',
      'repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      },
      false,
      true
    )
  ).toBeTruthy();

  expect(await calculate_sha256('test.dat')).toEqual(
    await calculate_sha256('repertory_test.dat')
  );

  expect(await api.file.delete('/repertory_test.dat')).toEqual(0);
  fs.unlinkSync('repertory_test.dat');

  await conn.disconnect();
}, 60000);

test('can resume upload using api', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);

  const fd = fs.openSync('test.dat', 'r');
  const buffer = Buffer.alloc(1024);
  fs.readSync(fd, buffer, 0, buffer.length);
  fs.closeSync(fd);

  const f = await api.file.create_or_open('/repertory_test.dat');
  await f.write(0, buffer);
  await f.close();

  expect(
    await api.file.upload(
      'test.dat',
      '/repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      },
      false,
      true
    )
  ).toBeTruthy();

  expect(
    await api.file.download(
      '/repertory_test.dat',
      'repertory_test.dat',
      (l, r, p, c) => {
        console.log(l, r, p, c);
      }
    )
  ).toBeTruthy();

  expect(await calculate_sha256('test.dat')).toEqual(
    await calculate_sha256('repertory_test.dat')
  );

  expect(await api.file.delete('/repertory_test.dat')).toEqual(0);
  fs.unlinkSync('repertory_test.dat');

  await conn.disconnect();
}, 60000);

test('exists returns false if directory is not found', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(await api.directory.exists('/cow')).toEqual(false);
  await conn.disconnect();
});

test('exists returns false if file is not found', async () => {
  const conn = await repertory.create_pool(
    2,
    TEST_HOST,
    TEST_PORT,
    TEST_PASSWORD
  );
  const api = repertory.create_api(conn);
  expect(await api.file.exists('/cow')).toEqual(false);
  await conn.disconnect();
});
