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 "../crypto/keyring/account_self";
import { 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
 *
 */

fdescribe("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_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: 1,
          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();
              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();
      });
    });
  });
});
