import { TestBed, waitForAsync } from '@angular/core/testing';
import { HttpClient, HttpHeaders, HttpClientModule } from '@angular/common/http';
import { CookieService } from 'ngx-cookie';
import { base64 } from 'rfc4648';
import { CookieModule } from 'ngx-cookie';

import msgpack from 'msgpack-lite';
import { SelfAccountKeyring } from './keyring/account_self';
import { AbstractAccountKeyring, AbstractSelfAccountKeyring } from './keyring/account_base';
import {
  AbstractPrivateKeyring,
  PrivateKeyringV1,
  PrivateKeyringV2,
  PRIVATE_KEYRING_CLASSES,
} from './keyring/private';
import { RoomKeyring } from './keyring/room';
import { AccountTrustConfigDumpArgs } from './context/account_trust/args';
import { PeerAccountKeyring } from './keyring/account_peer';
import { AccountSignatureConfigDumpArgs } from './context/account_signature/args';

import { dump_raw_keyring } from './cryptography-context.spec';
import { ACCOUNT_SIGNATURE_CRYPTO_CONTEXT } from './context/account_signature/__init__';
import { ACCOUNT_TRUST_CRYPTO_CONTEXT } from './context/account_trust/__init__';
import { environment } from 'src/environments/environment';

/**
 * check the object structure, and Uint8Arrays eq.
 */
export function isObjectTheSame(obj1, obj2): boolean {
  if (Object.keys(obj1).length != Object.keys(obj2).length) return false;

  for (let key in obj1) {
    if (!obj2.hasOwnProperty(key)) return false;
  }

  for (let key in obj1) {
    if (!obj1[key] || !obj2[key]) {
      // check null, undefined
      if (obj1[key] !== obj2[key]) return false;
    } else {
      if (obj1[key] instanceof Uint8Array) {
        if (base64.stringify(obj1[key]) != base64.stringify(obj2[key])) return false;
      } else if (obj1[key].constructor != Object) {
        if (obj1[key] !== obj2[key]) return false;
      } else {
        if (!isObjectTheSame(obj1[key], obj2[key])) return false;
      }
    }
  }

  return true;
}

/**
 *
 * THIS FILE IS TESTING THE BOTTOM/NATIVE LAYER OF OUR CRYPTO FUNCIONALITY
 * WE MADE A HANDY INTERFACE FOR THIS TO USE: global-crypto.ts
 *
 */

describe('Cryptography keyring test', () => {
  let cookie;
  let conf: any = {};

  let options = {
    headers: new HttpHeaders({ 'Content-type': 'application/msgpack' }),
    responseType: 'arraybuffer' as 'arraybuffer',
  };

  let http: HttpClient;
  let apiUrl = environment.url_api + '/';

  let expectedList = ['account_peer', 'account_self', 'private_v1', 'private_v2', 'room'];

  let csrfToken = '';

  const nfd_e = 'éé'; // e\xcc\x81\xc3\xa9
  const nfc_e = 'éé'; // \xc3\xa9\xc3\xa9

  let textEncoder = new TextEncoder();
  let textDecoder = new TextDecoder();

  beforeAll((done) => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule, CookieModule.forRoot()],
      providers: [CookieService],
    });

    http = TestBed.inject(HttpClient);
    cookie = TestBed.inject(CookieService);

    http.get(apiUrl + 'debug/crypto/keyring/configs', options).subscribe((res) => {
      conf = msgpack.decode(new Uint8Array(res));
      console.log(conf);

      // set the cookie
      http.get(apiUrl + 'r/csrf', options).subscribe((res) => {
        csrfToken = cookie.get('__Secure-csrf');

        options = {
          headers: new HttpHeaders({
            'Content-type': 'application/msgpack',
            pcsrf: csrfToken,
          }),
          responseType: 'arraybuffer' as 'arraybuffer',
        };

        done();
      });
    });
  });

  it('check deep-eq tester function', () => {
    expect(isObjectTheSame({}, {})).toBeTrue();
    expect(isObjectTheSame({ a: 1 }, { a: 1 })).toBeTrue();
    expect(
      isObjectTheSame({ a: new Uint8Array([1, 2]) }, { a: new Uint8Array([1, 2]) })
    ).toBeTrue();
    expect(isObjectTheSame({ a: undefined }, { a: undefined })).toBeTrue();
    expect(isObjectTheSame({ a: null }, { a: null })).toBeTrue();
    expect(isObjectTheSame({ a: { a: { a: 1 } } }, { a: { a: { a: 1 } } })).toBeTrue();
    expect(isObjectTheSame({ a: 1, b: 1 }, { a: 1, b: 1 })).toBeTrue();
    expect(isObjectTheSame({ a: '1', b: 1 }, { a: '1', b: 1 })).toBeTrue();
    expect(
      isObjectTheSame({ a: '1', b: 1, c: { d: 1 } }, { a: '1', b: 1, c: { d: 1 } })
    ).toBeTrue();

    expect(
      isObjectTheSame({ a: new Uint8Array([1, 2]) }, { a: new Uint8Array([1, 3]) })
    ).toBeFalse();
    expect(isObjectTheSame({ a: undefined }, { a: null })).toBeFalse();
    expect(isObjectTheSame({ a: 1 }, { a: null })).toBeFalse();
    expect(isObjectTheSame({ a: 1 }, { a: undefined })).toBeFalse();
    expect(isObjectTheSame({ a: { a: { a: 1 } } }, { a: { a: { a: 2 } } })).toBeFalse();
    expect(isObjectTheSame({ a: 1 }, { a: 2 })).toBeFalse();
    expect(isObjectTheSame({ a: 1 }, { a: '1' })).toBeFalse();
    expect(isObjectTheSame({ a: 1 }, { a: 1, b: 1 })).toBeFalse();
  });

  // check if we can get the conf list from server
  it('should get server conf', () => {
    console.log(conf);
    expect(conf).toBeTruthy();
  });

  // if this arr is changed, we should write more tests for the new elements
  it('check the conf values', (done) => {
    expect(expectedList.join() == conf['keyrings'].join()).toBeTrue();

    done();
  });

  it('account_base - 1', (done) => {
    expect(AbstractAccountKeyring.TRUST_KEY_VERSIONS).toEqual(['2']);
    expect(AbstractAccountKeyring.VALID_PUBLIC_KEY_LENGTH_MAP).toEqual({ '1': 32, '2': 32 });
    expect(AbstractAccountKeyring.ECDH_KEY_VERSIONS).toEqual(['1']);

    done();
  });

  it('account_self - 1', (done) => {
    PrivateKeyringV1.load(textEncoder.encode('priv_kr'), textEncoder.encode('salt')).then(
      (priv_kr0) => {
        let acc_kr0 = SelfAccountKeyring.generate('TestAcc0');

        acc_kr0.dump(priv_kr0).then((dump) => {
          let master = dump[AbstractSelfAccountKeyring.MASTER];
          let enc = dump[AbstractSelfAccountKeyring.CURVE25519];
          let sign = dump[AbstractSelfAccountKeyring.ED25519];

          let param = msgpack.encode({
            name: 'account_self',
            version: 1,
            args: [dump_raw_keyring(priv_kr0), textDecoder.decode(acc_kr0.id), master, enc, sign],
          });

          http
            .post(apiUrl + 'debug/crypto/keyring/load', new Uint8Array(param).buffer, options)
            .toPromise()
            .then((res) => {
              let expectedLoad = msgpack.decode(new Uint8Array(res)).result;

              SelfAccountKeyring.load(
                priv_kr0,
                textDecoder.decode(acc_kr0.id),
                master,
                enc,
                sign
              ).then((acc_kr) => {
                expect(isObjectTheSame(expectedLoad, dump_raw_keyring(acc_kr))).toBeTrue();
                done();
              });
            });
        });
      }
    );
  });

  it('account_self - 2', (done) => {
    PrivateKeyringV1.load(textEncoder.encode('priv_kr'), textEncoder.encode('salt')).then(
      (priv_kr0) => {
        let acc_kr0 = SelfAccountKeyring.generate('TestAcc0');

        acc_kr0.dump(priv_kr0).then((dump) => {
          let master = dump[AbstractSelfAccountKeyring.MASTER];

          acc_kr0.dump().then((dump) => {
            let enc = dump[AbstractSelfAccountKeyring.CURVE25519];
            let sign = dump[AbstractSelfAccountKeyring.ED25519];

            let param = msgpack.encode({
              name: 'account_self',
              version: 1,
              args: [dump_raw_keyring(priv_kr0), textDecoder.decode(acc_kr0.id), master, enc, sign],
            });

            http
              .post(apiUrl + 'debug/crypto/keyring/load', new Uint8Array(param).buffer, options)
              .toPromise()
              .then((res) => {
                let expectedLoad = msgpack.decode(new Uint8Array(res)).result;

                SelfAccountKeyring.load(
                  priv_kr0,
                  textDecoder.decode(acc_kr0.id),
                  master,
                  enc,
                  sign
                ).then((acc_kr) => {
                  expect(isObjectTheSame(expectedLoad, dump_raw_keyring(acc_kr))).toBeTrue();
                  done();
                });
              });
          });
        });
      }
    );
  });

  it('account_peer - 1', (done) => {
    let acc_kr0 = SelfAccountKeyring.generate('TestAcc0');

    ACCOUNT_SIGNATURE_CRYPTO_CONTEXT.dump(
      new AccountSignatureConfigDumpArgs(
        acc_kr0.get_public_key(PeerAccountKeyring.CURVE25519),
        acc_kr0
      )
    ).then((accountSignDump) => {
      let param = msgpack.encode({
        name: 'account_peer',
        version: 1,
        args: [
          textDecoder.decode(acc_kr0.id),
          acc_kr0.get_public_key(PeerAccountKeyring.CURVE25519),
          accountSignDump,
          acc_kr0.get_public_key(PeerAccountKeyring.ED25519),
        ],
      });

      http
        .post(apiUrl + 'debug/crypto/keyring/load', new Uint8Array(param).buffer, options)
        .toPromise()
        .then((res) => {
          let expectedLoad = msgpack.decode(new Uint8Array(res)).result;

          PeerAccountKeyring.load(
            textDecoder.decode(acc_kr0.id),
            acc_kr0.get_public_key(PeerAccountKeyring.CURVE25519),
            accountSignDump,
            acc_kr0.get_public_key(PeerAccountKeyring.ED25519)
          ).then((peer_kr) => {
            expect(isObjectTheSame(expectedLoad, dump_raw_keyring(peer_kr))).toBeTrue();
            done();
          });
        });
    });
  });

  it('account_peer - 2', (done) => {
    let acc_kr0 = SelfAccountKeyring.generate('TestAcc0');
    let acc_kr1 = SelfAccountKeyring.generate('TestAcc1');

    ACCOUNT_SIGNATURE_CRYPTO_CONTEXT.dump(
      new AccountSignatureConfigDumpArgs(
        acc_kr0.get_public_key(PeerAccountKeyring.CURVE25519),
        acc_kr0
      )
    ).then((accountSignDump) => {
      ACCOUNT_TRUST_CRYPTO_CONTEXT.dump(
        new AccountTrustConfigDumpArgs(acc_kr1, acc_kr0, PeerAccountKeyring.ED25519)
      ).then((accTrustDump) => {
        let param = msgpack.encode({
          name: 'account_peer',
          version: 2,
          args: [
            textDecoder.decode(acc_kr0.id),
            acc_kr0.get_public_key(PeerAccountKeyring.CURVE25519),
            accountSignDump,
            acc_kr0.get_public_key(PeerAccountKeyring.ED25519),
            accTrustDump,
            dump_raw_keyring(acc_kr1),
          ],
        });

        http
          .post(apiUrl + 'debug/crypto/keyring/load', new Uint8Array(param).buffer, options)
          .toPromise()
          .then((res) => {
            let expectedLoad = msgpack.decode(new Uint8Array(res)).result;

            PeerAccountKeyring.load(
              textDecoder.decode(acc_kr0.id),
              acc_kr0.get_public_key(PeerAccountKeyring.CURVE25519),
              accountSignDump,
              acc_kr0.get_public_key(PeerAccountKeyring.ED25519),
              accTrustDump,
              acc_kr1
            ).then((peer_kr) => {
              expect(isObjectTheSame(expectedLoad, dump_raw_keyring(peer_kr))).toBeTrue();
              expect(peer_kr.trusted).toEqual(PeerAccountKeyring.TRUSTED_FULLY);

              accTrustDump[accTrustDump.length - 2] ^= 64;  // break the trust value
              PeerAccountKeyring.load(
                textDecoder.decode(acc_kr0.id),
                acc_kr0.get_public_key(PeerAccountKeyring.CURVE25519),
                accountSignDump,
                acc_kr0.get_public_key(PeerAccountKeyring.ED25519),
                accTrustDump,
                acc_kr1
              ).then((bad_peer_kr) => {
                expect(bad_peer_kr.trusted).toEqual(PeerAccountKeyring.TRUSTED_NOT);
                done();
              });
            });
          });
      });
    });
  });

  it('private_normalize_password', (done) => {
    expect(AbstractPrivateKeyring.normalize_password('password')).toEqual('password');
    expect(
      AbstractPrivateKeyring.normalize_password(
        '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ '
      )
    ).toEqual(
      '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ '
    );
    expect(nfd_e).not.toEqual(nfc_e);
    expect(AbstractPrivateKeyring.normalize_password(nfd_e)).toEqual(nfc_e); // NFD -> NFC
    expect(AbstractPrivateKeyring.normalize_password('\t\n\r\x0b\x0c')).toEqual('     ');
    expect(
      AbstractPrivateKeyring.normalize_password(
        '\u0009\u000A\u000B\u000C\u000D\u001C\u001D\u001E\u001F\u0020\u0085\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000'
      )
    ).toEqual('                             ');
    expect(AbstractPrivateKeyring.normalize_password('_\x00.')).toEqual('_\x00.');

    done();
  });

  it('private_v1', (done) => {
    let password = textEncoder.encode('password');
    let salt = textEncoder.encode('password');

    expect(PrivateKeyringV1.password_forms(nfd_e)).toEqual([nfc_e, nfd_e]);
    expect(PrivateKeyringV1.password_forms(nfc_e)).toEqual([nfc_e]);

    let param = msgpack.encode({
      name: 'private_v1',
      version: 1,
      args: [password, salt],
    });

    http
      .post(apiUrl + 'debug/crypto/keyring/load', new Uint8Array(param).buffer, options)
      .toPromise()
      .then((res) => {
        let expectedLoad = msgpack.decode(new Uint8Array(res)).result;

        PrivateKeyringV1.load(password, salt).then((priv_kr) => {
          expect(isObjectTheSame(expectedLoad, dump_raw_keyring(priv_kr))).toBeTrue();
          done();
        });
      });
  });

  it('private_flattened_password_forms_with_cls', (done) => {
    // flats = [AbstractPrivateKeyring.flattened_password_forms_with_cls(PRIVATE_KEYRING_CLASSES, pw) for pw in PASSWORDS]
    // assert flats == [
    //     [(PrivateKeyringV2, "jkléé"), (PrivateKeyringV1, "jkléé"), (PrivateKeyringV1, "jkléé")],
    //     [(PrivateKeyringV2, "jkléé"), (PrivateKeyringV1, "jkléé")],
    //     [(PrivateKeyringV2, "     "), (PrivateKeyringV1, "     "), (PrivateKeyringV1, "\t\n\r\x0b\x0c")],
    //     [(PrivateKeyringV2, "_-\x00\x01.!"), (PrivateKeyringV1, "_-\x00\x01.!")],
    // ], str([[(cls.__name__, pw) for cls, pw in flat] for flat in flats])

    let PASSWORDS = ['jkléé', 'jkléé', '\t\n\r\x0b\x0c', '_-\x00\x01.!'];
    expect(PASSWORDS[0]).not.toEqual(PASSWORDS[1]);

    let expectValues: [typeof AbstractPrivateKeyring, string][][] = [
      [
        [PrivateKeyringV2, 'jkléé'],
        [PrivateKeyringV1, 'jkléé'],
        [PrivateKeyringV1, 'jkléé'],
      ],
      [
        [PrivateKeyringV2, 'jkléé'],
        [PrivateKeyringV1, 'jkléé'],
      ],
      [
        [PrivateKeyringV2, '     '],
        [PrivateKeyringV1, '     '],
        [PrivateKeyringV1, '\t\n\r\x0b\x0c'],
      ],
      [
        [PrivateKeyringV2, '_-\x00\x01.!'],
        [PrivateKeyringV1, '_-\x00\x01.!'],
      ],
    ];

    for (let pwIndex = 0; pwIndex < PASSWORDS.length; pwIndex++) {
      let flats = AbstractPrivateKeyring.flattened_password_forms_with_cls(
        PRIVATE_KEYRING_CLASSES,
        PASSWORDS[pwIndex]
      );

      for (let flatIndex = 0; flatIndex < flats.length; flatIndex++) {
        expect(flats[flatIndex][0] === expectValues[pwIndex][flatIndex][0]).toBeTrue();
        console.log(
          flats[flatIndex][1],
          expectValues[pwIndex][flatIndex][1],
          flats[flatIndex][1] === expectValues[pwIndex][flatIndex][1]
        );
        expect(flats[flatIndex][1] === expectValues[pwIndex][flatIndex][1]).toBeTrue();
      }
    }

    done();
  });

  it('room - 1', (done) => {
    let acc_kr0 = SelfAccountKeyring.generate('TestAcc0');
    let acc_kr1 = SelfAccountKeyring.generate('TestAcc1');
    let room_kr = RoomKeyring.generate('TestRoom');

    room_kr.dump(acc_kr0, acc_kr1).then((keys) => {
      let param = msgpack.encode({
        name: 'room',
        version: 1,
        args: [
          dump_raw_keyring(acc_kr0),
          dump_raw_keyring(acc_kr1),
          textDecoder.decode(room_kr.id),
          undefined,
          keys[RoomKeyring.PINNED],
        ],
      });

      http
        .post(apiUrl + 'debug/crypto/keyring/load', new Uint8Array(param).buffer, options)
        .toPromise()
        .then((res) => {
          let expectedLoad = msgpack.decode(new Uint8Array(res)).result;

          RoomKeyring.load(
            acc_kr0,
            acc_kr1,
            textDecoder.decode(room_kr.id),
            undefined,
            keys[RoomKeyring.PINNED]
          ).then((room_kr) => {
            expect(isObjectTheSame(expectedLoad, dump_raw_keyring(room_kr))).toBeTrue();
            done();
          });
        });
    });
  });

  it('room - 2', (done) => {
    let acc_kr0 = SelfAccountKeyring.generate('TestAcc0');
    let acc_kr1 = SelfAccountKeyring.generate('TestAcc1');
    let room_kr = RoomKeyring.generate('TestRoom');

    room_kr.dump(acc_kr0, acc_kr0).then((keys) => {
      let param = msgpack.encode({
        name: 'room',
        version: 1,
        args: [
          dump_raw_keyring(acc_kr0),
          dump_raw_keyring(acc_kr0),
          textDecoder.decode(room_kr.id),
          undefined,
          keys[RoomKeyring.PINNED],
        ],
      });

      http
        .post(apiUrl + 'debug/crypto/keyring/load', new Uint8Array(param).buffer, options)
        .toPromise()
        .then((res) => {
          let expectedLoad = msgpack.decode(new Uint8Array(res)).result;

          RoomKeyring.load(
            acc_kr0,
            acc_kr0,
            textDecoder.decode(room_kr.id),
            undefined,
            keys[RoomKeyring.PINNED]
          ).then((room_kr) => {
            expect(isObjectTheSame(expectedLoad, dump_raw_keyring(room_kr))).toBeTrue();
            done();
          });
        });
    });
  });

  it('room - 3', (done) => {
    let acc_kr0 = SelfAccountKeyring.generate('TestAcc0');
    let acc_kr1 = SelfAccountKeyring.generate('TestAcc1');
    let room_kr = RoomKeyring.generate('TestRoom');

    room_kr.dump(acc_kr0, acc_kr0).then((keys) => {
      let param = msgpack.encode({
        name: 'room',
        version: 1,
        args: [
          dump_raw_keyring(acc_kr0),
          dump_raw_keyring(acc_kr1),
          textDecoder.decode(room_kr.id),
          keys[RoomKeyring.PINNED],
          undefined,
        ],
      });

      http
        .post(apiUrl + 'debug/crypto/keyring/load', new Uint8Array(param).buffer, options)
        .toPromise()
        .then((res) => {
          let expectedLoad = msgpack.decode(new Uint8Array(res)).result;

          RoomKeyring.load(
            acc_kr0,
            acc_kr1,
            textDecoder.decode(room_kr.id),
            keys[RoomKeyring.PINNED],
            undefined
          ).then((room_kr) => {
            expect(isObjectTheSame(expectedLoad, dump_raw_keyring(room_kr))).toBeTrue();
            done();
          });
        });
    });
  });

  it('room - 4', (done) => {
    let acc_kr0 = SelfAccountKeyring.generate('TestAcc0');
    let acc_kr1 = SelfAccountKeyring.generate('TestAcc0');
    let room_kr = RoomKeyring.generate('TestRoom');

    room_kr.dump(acc_kr0, acc_kr1).then((keys) => {
      RoomKeyring.load(
        acc_kr0,
        acc_kr1,
        textDecoder.decode(room_kr.id),
        undefined,
        keys[RoomKeyring.PINNED]
      ).then((loadedRoomKr) => {
        console.log('ok test', room_kr, loadedRoomKr);
        expect(
          base64.stringify(room_kr.id) == base64.stringify(loadedRoomKr.id) &&
            base64.stringify(room_kr.get_key(RoomKeyring.PINNED)) ==
              base64.stringify(loadedRoomKr.get_key(RoomKeyring.PINNED))
        ).toBeTrue();
        done();
      });
    });
  });
});
