const anchor = require("@project-serum/anchor");
const serumCmn = require("@project-serum/common");
const { TOKEN_PROGRAM_ID } = require("@solana/spl-token");
const utils = require("./utils");
const { assert, expect } = require("chai");
const nativeAssert = require("assert");

anchor.utils.features.set("anchor-deprecated-state");

describe("Lockup and Registry", () => {
  // Read the provider from the configured environmnet.
  const provider = anchor.AnchorProvider.env();
  // hack so we don't have to update serum-common library
  // to the new AnchorProvider class and Provider interface
  provider.send = provider.sendAndConfirm;

  // Configure the client to use the provider.
  anchor.setProvider(provider);

  const lockup = anchor.workspace.Lockup;
  const registry = anchor.workspace.Registry;

  let lockupAddress = null;
  const WHITELIST_SIZE = 10;

  let mint = null;
  let god = null;

  it("Sets up initial test state", async () => {
    const [_mint, _god] = await serumCmn.createMintAndVault(
      provider,
      new anchor.BN(1000000)
    );
    mint = _mint;
    god = _god;
  });

  it("Is initialized!", async () => {
    await lockup.state.rpc.new({
      accounts: {
        authority: provider.wallet.publicKey,
      },
    });

    lockupAddress = await lockup.state.address();
    const lockupAccount = await lockup.state.fetch();

    assert.isTrue(lockupAccount.authority.equals(provider.wallet.publicKey));
    assert.strictEqual(lockupAccount.whitelist.length, WHITELIST_SIZE);
    lockupAccount.whitelist.forEach((e) => {
      assert.isTrue(e.programId.equals(anchor.web3.PublicKey.default));
    });
  });

  it("Deletes the default whitelisted addresses", async () => {
    const defaultEntry = { programId: anchor.web3.PublicKey.default };
    await lockup.state.rpc.whitelistDelete(defaultEntry, {
      accounts: {
        authority: provider.wallet.publicKey,
      },
    });
  });

  it("Sets a new authority", async () => {
    const newAuthority = anchor.web3.Keypair.generate();
    await lockup.state.rpc.setAuthority(newAuthority.publicKey, {
      accounts: {
        authority: provider.wallet.publicKey,
      },
    });

    let lockupAccount = await lockup.state.fetch();
    assert.isTrue(lockupAccount.authority.equals(newAuthority.publicKey));

    await lockup.state.rpc.setAuthority(provider.wallet.publicKey, {
      accounts: {
        authority: newAuthority.publicKey,
      },
      signers: [newAuthority],
    });

    lockupAccount = await lockup.state.fetch();
    assert.isTrue(lockupAccount.authority.equals(provider.wallet.publicKey));
  });

  const entries = [];

  it("Adds to the whitelist", async () => {
    const generateEntry = async () => {
      let programId = anchor.web3.Keypair.generate().publicKey;
      return {
        programId,
      };
    };

    for (let k = 0; k < WHITELIST_SIZE; k += 1) {
      entries.push(await generateEntry());
    }

    const accounts = {
      authority: provider.wallet.publicKey,
    };

    await lockup.state.rpc.whitelistAdd(entries[0], { accounts });

    let lockupAccount = await lockup.state.fetch();

    assert.lengthOf(lockupAccount.whitelist, 1);
    assert.deepEqual(lockupAccount.whitelist, [entries[0]]);

    for (let k = 1; k < WHITELIST_SIZE; k += 1) {
      await lockup.state.rpc.whitelistAdd(entries[k], { accounts });
    }

    lockupAccount = await lockup.state.fetch();

    assert.deepEqual(lockupAccount.whitelist, entries);

    await nativeAssert.rejects(
      async () => {
        const e = await generateEntry();
        await lockup.state.rpc.whitelistAdd(e, { accounts });
      },
      (err) => {
        assert.strictEqual(err.error.errorCode.number, 6008);
        assert.strictEqual(err.error.errorMessage, "Whitelist is full");
        return true;
      }
    );
  });

  it("Removes from the whitelist", async () => {
    await lockup.state.rpc.whitelistDelete(entries[0], {
      accounts: {
        authority: provider.wallet.publicKey,
      },
    });
    let lockupAccount = await lockup.state.fetch();
    assert.deepStrictEqual(lockupAccount.whitelist, entries.slice(1));
  });

  const vesting = anchor.web3.Keypair.generate();
  let vestingAccount = null;
  let vestingSigner = null;

  it("Creates a vesting account", async () => {
    const startTs = new anchor.BN(Date.now() / 1000);
    const endTs = new anchor.BN(startTs.toNumber() + 5);
    const periodCount = new anchor.BN(2);
    const beneficiary = provider.wallet.publicKey;
    const depositAmount = new anchor.BN(100);

    const vault = anchor.web3.Keypair.generate();
    let [_vestingSigner, nonce] =
      await anchor.web3.PublicKey.findProgramAddress(
        [vesting.publicKey.toBuffer()],
        lockup.programId
      );
    vestingSigner = _vestingSigner;

    await lockup.rpc.createVesting(
      beneficiary,
      depositAmount,
      nonce,
      startTs,
      endTs,
      periodCount,
      null, // Lock realizor is None.
      {
        accounts: {
          vesting: vesting.publicKey,
          vault: vault.publicKey,
          depositor: god,
          depositorAuthority: provider.wallet.publicKey,
          tokenProgram: TOKEN_PROGRAM_ID,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
        },
        signers: [vesting, vault],
        instructions: [
          await lockup.account.vesting.createInstruction(vesting),
          ...(await serumCmn.createTokenAccountInstrs(
            provider,
            vault.publicKey,
            mint,
            vestingSigner
          )),
        ],
      }
    );

    vestingAccount = await lockup.account.vesting.fetch(vesting.publicKey);

    assert.isTrue(vestingAccount.beneficiary.equals(provider.wallet.publicKey));
    assert.isTrue(vestingAccount.mint.equals(mint));
    assert.isTrue(vestingAccount.grantor.equals(provider.wallet.publicKey));
    assert.isTrue(vestingAccount.outstanding.eq(depositAmount));
    assert.isTrue(vestingAccount.startBalance.eq(depositAmount));
    assert.isTrue(vestingAccount.whitelistOwned.eq(new anchor.BN(0)));
    assert.strictEqual(vestingAccount.nonce, nonce);
    assert.isTrue(vestingAccount.createdTs.gt(new anchor.BN(0)));
    assert.isTrue(vestingAccount.startTs.eq(startTs));
    assert.isTrue(vestingAccount.endTs.eq(endTs));
    assert.isNull(vestingAccount.realizor);
  });

  it("Fails to withdraw from a vesting account before vesting", async () => {
    await nativeAssert.rejects(
      async () => {
        await lockup.rpc.withdraw(new anchor.BN(100), {
          accounts: {
            vesting: vesting.publicKey,
            beneficiary: provider.wallet.publicKey,
            token: god,
            vault: vestingAccount.vault,
            vestingSigner: vestingSigner,
            tokenProgram: TOKEN_PROGRAM_ID,
            clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
          },
        });
      },
      (err) => {
        assert.strictEqual(err.error.errorCode.number, 6007);
        assert.strictEqual(
          err.error.errorMessage,
          "Insufficient withdrawal balance."
        );
        return true;
      }
    );
  });

  it("Waits for a vesting period to pass", async () => {
    await serumCmn.sleep(10 * 1000);
  });

  it("Withdraws from the vesting account", async () => {
    const token = await serumCmn.createTokenAccount(
      provider,
      mint,
      provider.wallet.publicKey
    );

    await lockup.rpc.withdraw(new anchor.BN(100), {
      accounts: {
        vesting: vesting.publicKey,
        beneficiary: provider.wallet.publicKey,
        token,
        vault: vestingAccount.vault,
        vestingSigner,
        tokenProgram: TOKEN_PROGRAM_ID,
        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
      },
    });

    vestingAccount = await lockup.account.vesting.fetch(vesting.publicKey);
    assert.isTrue(vestingAccount.outstanding.eq(new anchor.BN(0)));

    const vaultAccount = await serumCmn.getTokenAccount(
      provider,
      vestingAccount.vault
    );
    assert.isTrue(vaultAccount.amount.eq(new anchor.BN(0)));

    const tokenAccount = await serumCmn.getTokenAccount(provider, token);
    assert.isTrue(tokenAccount.amount.eq(new anchor.BN(100)));
  });

  const registrar = anchor.web3.Keypair.generate();
  const rewardQ = anchor.web3.Keypair.generate();
  const withdrawalTimelock = new anchor.BN(4);
  const stakeRate = new anchor.BN(2);
  const rewardQLen = 170;
  let registrarAccount = null;
  let registrarSigner = null;
  let nonce = null;
  let poolMint = null;

  it("Creates registry genesis", async () => {
    const [_registrarSigner, _nonce] =
      await anchor.web3.PublicKey.findProgramAddress(
        [registrar.publicKey.toBuffer()],
        registry.programId
      );
    registrarSigner = _registrarSigner;
    nonce = _nonce;
    poolMint = await serumCmn.createMint(provider, registrarSigner);
  });

  it("Initializes registry's global state", async () => {
    await registry.state.rpc.new({
      accounts: { lockupProgram: lockup.programId },
    });

    const state = await registry.state.fetch();
    assert.isTrue(state.lockupProgram.equals(lockup.programId));

    // Should not allow a second initializatoin.
    await nativeAssert.rejects(
      async () => {
        await registry.state.rpc.new(lockup.programId);
      },
      (err) => {
        return true;
      }
    );
  });

  it("Initializes the registrar", async () => {
    await registry.rpc.initialize(
      mint,
      provider.wallet.publicKey,
      nonce,
      withdrawalTimelock,
      stakeRate,
      rewardQLen,
      {
        accounts: {
          registrar: registrar.publicKey,
          poolMint,
          rewardEventQ: rewardQ.publicKey,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
        },
        signers: [registrar, rewardQ],
        instructions: [
          await registry.account.registrar.createInstruction(registrar),
          await registry.account.rewardQueue.createInstruction(rewardQ, 8250),
        ],
      }
    );

    registrarAccount = await registry.account.registrar.fetch(
      registrar.publicKey
    );

    assert.isTrue(registrarAccount.authority.equals(provider.wallet.publicKey));
    assert.strictEqual(registrarAccount.nonce, nonce);
    assert.isTrue(registrarAccount.mint.equals(mint));
    assert.isTrue(registrarAccount.poolMint.equals(poolMint));
    assert.isTrue(registrarAccount.stakeRate.eq(stakeRate));
    assert.isTrue(registrarAccount.rewardEventQ.equals(rewardQ.publicKey));
    assert.isTrue(registrarAccount.withdrawalTimelock.eq(withdrawalTimelock));
  });

  const member = anchor.web3.Keypair.generate();
  let memberAccount = null;
  let memberSigner = null;
  let balances = null;
  let balancesLocked = null;

  it("Creates a member", async () => {
    const [_memberSigner, nonce] =
      await anchor.web3.PublicKey.findProgramAddress(
        [registrar.publicKey.toBuffer(), member.publicKey.toBuffer()],
        registry.programId
      );
    memberSigner = _memberSigner;

    const [mainTx, _balances] = await utils.createBalanceSandbox(
      provider,
      registrarAccount,
      memberSigner
    );
    const [lockedTx, _balancesLocked] = await utils.createBalanceSandbox(
      provider,
      registrarAccount,
      memberSigner
    );

    balances = _balances;
    balancesLocked = _balancesLocked;

    const tx = registry.transaction.createMember(nonce, {
      accounts: {
        registrar: registrar.publicKey,
        member: member.publicKey,
        beneficiary: provider.wallet.publicKey,
        memberSigner,
        balances,
        balancesLocked,
        tokenProgram: TOKEN_PROGRAM_ID,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
      },
      instructions: [await registry.account.member.createInstruction(member)],
    });

    const signers = [member, provider.wallet.payer];

    const allTxs = [mainTx, lockedTx, { tx, signers }];

    let txSigs = await provider.sendAll(allTxs);

    memberAccount = await registry.account.member.fetch(member.publicKey);

    assert.isTrue(memberAccount.registrar.equals(registrar.publicKey));
    assert.isTrue(memberAccount.beneficiary.equals(provider.wallet.publicKey));
    assert.isTrue(memberAccount.metadata.equals(anchor.web3.PublicKey.default));
    assert.strictEqual(
      JSON.stringify(memberAccount.balances),
      JSON.stringify(balances)
    );
    assert.strictEqual(
      JSON.stringify(memberAccount.balancesLocked),
      JSON.stringify(balancesLocked)
    );
    assert.strictEqual(memberAccount.rewardsCursor, 0);
    assert.isTrue(memberAccount.lastStakeTs.eq(new anchor.BN(0)));
  });

  it("Deposits (unlocked) to a member", async () => {
    const depositAmount = new anchor.BN(120);
    await registry.rpc.deposit(depositAmount, {
      accounts: {
        depositor: god,
        depositorAuthority: provider.wallet.publicKey,
        tokenProgram: TOKEN_PROGRAM_ID,
        vault: memberAccount.balances.vault,
        beneficiary: provider.wallet.publicKey,
        member: member.publicKey,
      },
    });

    const memberVault = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.vault
    );
    assert.isTrue(memberVault.amount.eq(depositAmount));
  });

  /*
  it("Stakes to a member (unlocked)", async () => {
    const stakeAmount = new anchor.BN(10);
    await registry.rpc.stake(stakeAmount, false, {
      accounts: {
        // Stake instance.
        registrar: registrar.publicKey,
        rewardEventQ: rewardQ.publicKey,
        poolMint,
        // Member.
        member: member.publicKey,
        beneficiary: provider.wallet.publicKey,
        balances,
        balancesLocked,
        // Program signers.
        memberSigner,
        registrarSigner,
        // Misc.
        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
    });

    const vault = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.vault
    );
    const vaultStake = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.vaultStake
    );
    const spt = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.spt
    );

    assert.isTrue(vault.amount.eq(new anchor.BN(100)));
    assert.isTrue(vaultStake.amount.eq(new anchor.BN(20)));
    assert.isTrue(spt.amount.eq(new anchor.BN(10)));
  });

  const unlockedVendor = anchor.web3.Keypair.generate();
  const unlockedVendorVault = anchor.web3.Keypair.generate();
  let unlockedVendorSigner = null;

  it("Drops an unlocked reward", async () => {
    const rewardKind = {
      unlocked: {},
    };
    const rewardAmount = new anchor.BN(200);
    const expiry = new anchor.BN(Date.now() / 1000 + 5);
    const [_vendorSigner, nonce] =
      await anchor.web3.PublicKey.findProgramAddress(
        [registrar.publicKey.toBuffer(), unlockedVendor.publicKey.toBuffer()],
        registry.programId
      );
    unlockedVendorSigner = _vendorSigner;

    await registry.rpc.dropReward(
      rewardKind,
      rewardAmount,
      expiry,
      provider.wallet.publicKey,
      nonce,
      {
        accounts: {
          registrar: registrar.publicKey,
          rewardEventQ: rewardQ.publicKey,
          poolMint,

          vendor: unlockedVendor.publicKey,
          vendorVault: unlockedVendorVault.publicKey,

          depositor: god,
          depositorAuthority: provider.wallet.publicKey,

          tokenProgram: TOKEN_PROGRAM_ID,
          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
        },
        signers: [unlockedVendorVault, unlockedVendor],
        instructions: [
          ...(await serumCmn.createTokenAccountInstrs(
            provider,
            unlockedVendorVault.publicKey,
            mint,
            unlockedVendorSigner
          )),
          await registry.account.rewardVendor.createInstruction(unlockedVendor),
        ],
      }
    );

    const vendorAccount = await registry.account.rewardVendor.fetch(
      unlockedVendor.publicKey
    );

    assert.isTrue(vendorAccount.registrar.equals(registrar.publicKey));
    assert.isTrue(vendorAccount.vault.equals(unlockedVendorVault.publicKey));
    assert.strictEqual(vendorAccount.nonce, nonce);
    assert.isTrue(vendorAccount.poolTokenSupply.eq(new anchor.BN(10)));
    assert.isTrue(vendorAccount.expiryTs.eq(expiry));
    assert.isTrue(
      vendorAccount.expiryReceiver.equals(provider.wallet.publicKey)
    );
    assert.isTrue(vendorAccount.total.eq(rewardAmount));
    assert.isFalse(vendorAccount.expired);
    assert.strictEqual(vendorAccount.rewardEventQCursor, 0);
    assert.deepEqual(vendorAccount.kind, rewardKind);

    const rewardQAccount = await registry.account.rewardQueue.fetch(
      rewardQ.publicKey
    );
    assert.strictEqual(rewardQAccount.head, 1);
    assert.strictEqual(rewardQAccount.tail, 0);
    const e = rewardQAccount.events[0];
    assert.isTrue(e.vendor.equals(unlockedVendor.publicKey));
    assert.strictEqual(e.locked, false);
  });

  it("Collects an unlocked reward", async () => {
    const token = await serumCmn.createTokenAccount(
      provider,
      mint,
      provider.wallet.publicKey
    );
    await registry.rpc.claimReward({
      accounts: {
        to: token,
        cmn: {
          registrar: registrar.publicKey,

          member: member.publicKey,
          beneficiary: provider.wallet.publicKey,
          balances,
          balancesLocked,

          vendor: unlockedVendor.publicKey,
          vault: unlockedVendorVault.publicKey,
          vendorSigner: unlockedVendorSigner,

          tokenProgram: TOKEN_PROGRAM_ID,
          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
        },
      },
    });

    let tokenAccount = await serumCmn.getTokenAccount(provider, token);
    assert.isTrue(tokenAccount.amount.eq(new anchor.BN(200)));

    const memberAccount = await registry.account.member.fetch(member.publicKey);
    assert.strictEqual(memberAccount.rewardsCursor, 1);
  });

  const lockedVendor = anchor.web3.Keypair.generate();
  const lockedVendorVault = anchor.web3.Keypair.generate();
  let lockedVendorSigner = null;
  let lockedRewardAmount = null;
  let lockedRewardKind = null;

  it("Drops a locked reward", async () => {
    lockedRewardKind = {
      locked: {
        startTs: new anchor.BN(Date.now() / 1000),
        endTs: new anchor.BN(Date.now() / 1000 + 6),
        periodCount: new anchor.BN(2),
      },
    };
    lockedRewardAmount = new anchor.BN(200);
    const expiry = new anchor.BN(Date.now() / 1000 + 5);
    const [_vendorSigner, nonce] =
      await anchor.web3.PublicKey.findProgramAddress(
        [registrar.publicKey.toBuffer(), lockedVendor.publicKey.toBuffer()],
        registry.programId
      );
    lockedVendorSigner = _vendorSigner;

    await registry.rpc.dropReward(
      lockedRewardKind,
      lockedRewardAmount,
      expiry,
      provider.wallet.publicKey,
      nonce,
      {
        accounts: {
          registrar: registrar.publicKey,
          rewardEventQ: rewardQ.publicKey,
          poolMint,

          vendor: lockedVendor.publicKey,
          vendorVault: lockedVendorVault.publicKey,

          depositor: god,
          depositorAuthority: provider.wallet.publicKey,

          tokenProgram: TOKEN_PROGRAM_ID,
          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
        },
        signers: [lockedVendorVault, lockedVendor],
        instructions: [
          ...(await serumCmn.createTokenAccountInstrs(
            provider,
            lockedVendorVault.publicKey,
            mint,
            lockedVendorSigner
          )),
          await registry.account.rewardVendor.createInstruction(lockedVendor),
        ],
      }
    );

    const vendorAccount = await registry.account.rewardVendor.fetch(
      lockedVendor.publicKey
    );

    assert.isTrue(vendorAccount.registrar.equals(registrar.publicKey));
    assert.isTrue(vendorAccount.vault.equals(lockedVendorVault.publicKey));
    assert.strictEqual(vendorAccount.nonce, nonce);
    assert.isTrue(vendorAccount.poolTokenSupply.eq(new anchor.BN(10)));
    assert.isTrue(vendorAccount.expiryTs.eq(expiry));
    assert.isTrue(
      vendorAccount.expiryReceiver.equals(provider.wallet.publicKey)
    );
    assert.isTrue(vendorAccount.total.eq(lockedRewardAmount));
    assert.isFalse(vendorAccount.expired);
    assert.strictEqual(vendorAccount.rewardEventQCursor, 1);
    assert.strictEqual(
      JSON.stringify(vendorAccount.kind),
      JSON.stringify(lockedRewardKind)
    );

    const rewardQAccount = await registry.account.rewardQueue.fetch(
      rewardQ.publicKey
    );
    assert.strictEqual(rewardQAccount.head, 2);
    assert.strictEqual(rewardQAccount.tail, 0);
    const e = rewardQAccount.events[1];
    assert.isTrue(e.vendor.equals(lockedVendor.publicKey));
    assert.isTrue(e.locked);
  });

  let vendoredVesting = null;
  let vendoredVestingVault = null;
  let vendoredVestingSigner = null;

  it("Claims a locked reward", async () => {
    vendoredVesting = anchor.web3.Keypair.generate();
    vendoredVestingVault = anchor.web3.Keypair.generate();
    let [_vendoredVestingSigner, nonce] =
      await anchor.web3.PublicKey.findProgramAddress(
        [vendoredVesting.publicKey.toBuffer()],
        lockup.programId
      );
    vendoredVestingSigner = _vendoredVestingSigner;
    const remainingAccounts = lockup.instruction.createVesting
      .accounts({
        vesting: vendoredVesting.publicKey,
        vault: vendoredVestingVault.publicKey,
        depositor: lockedVendorVault.publicKey,
        depositorAuthority: lockedVendorSigner,
        tokenProgram: TOKEN_PROGRAM_ID,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
      })
      // Change the signer status on the vendor signer since it's signed by the program, not the
      // client.
      .map((meta) =>
        meta.pubkey === lockedVendorSigner ? { ...meta, isSigner: false } : meta
      );

    await registry.rpc.claimRewardLocked(nonce, {
      accounts: {
        registry: await registry.state.address(),
        lockupProgram: lockup.programId,
        cmn: {
          registrar: registrar.publicKey,

          member: member.publicKey,
          beneficiary: provider.wallet.publicKey,
          balances,
          balancesLocked,

          vendor: lockedVendor.publicKey,
          vault: lockedVendorVault.publicKey,
          vendorSigner: lockedVendorSigner,

          tokenProgram: TOKEN_PROGRAM_ID,
          clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
        },
      },
      remainingAccounts,
      signers: [vendoredVesting, vendoredVestingVault],
      instructions: [
        await lockup.account.vesting.createInstruction(vendoredVesting),
        ...(await serumCmn.createTokenAccountInstrs(
          provider,
          vendoredVestingVault.publicKey,
          mint,
          vendoredVestingSigner
        )),
      ],
    });

    const lockupAccount = await lockup.account.vesting.fetch(
      vendoredVesting.publicKey
    );

    assert.isTrue(lockupAccount.beneficiary.equals(provider.wallet.publicKey));
    assert.isTrue(lockupAccount.mint.equals(mint));
    assert.isTrue(lockupAccount.vault.equals(vendoredVestingVault.publicKey));
    assert.isTrue(lockupAccount.outstanding.eq(lockedRewardAmount));
    assert.isTrue(lockupAccount.startBalance.eq(lockedRewardAmount));
    assert.isTrue(lockupAccount.endTs.eq(lockedRewardKind.locked.endTs));
    assert.isTrue(
      lockupAccount.periodCount.eq(lockedRewardKind.locked.periodCount)
    );
    assert.isTrue(lockupAccount.whitelistOwned.eq(new anchor.BN(0)));
    assert.isTrue(lockupAccount.realizor.program.equals(registry.programId));
    assert.isTrue(lockupAccount.realizor.metadata.equals(member.publicKey));
  });

  it("Waits for the lockup period to pass", async () => {
    await serumCmn.sleep(10 * 1000);
  });

  it("Should fail to unlock an unrealized lockup reward", async () => {
    const token = await serumCmn.createTokenAccount(
      provider,
      mint,
      provider.wallet.publicKey
    );
    await nativeAssert.rejects(
      async () => {
        const withdrawAmount = new anchor.BN(10);
        await lockup.rpc.withdraw(withdrawAmount, {
          accounts: {
            vesting: vendoredVesting.publicKey,
            beneficiary: provider.wallet.publicKey,
            token,
            vault: vendoredVestingVault.publicKey,
            vestingSigner: vendoredVestingSigner,
            tokenProgram: TOKEN_PROGRAM_ID,
            clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
          },
          // TODO: trait methods generated on the client. Until then, we need to manually
          //       specify the account metas here.
          remainingAccounts: [
            { pubkey: registry.programId, isWritable: false, isSigner: false },
            { pubkey: member.publicKey, isWritable: false, isSigner: false },
            { pubkey: balances.spt, isWritable: false, isSigner: false },
            { pubkey: balancesLocked.spt, isWritable: false, isSigner: false },
          ],
        });
      },
      (err) => {
        // Solana doesn't propagate errors across CPI. So we receive the registry's error code,
        // not the lockup's.
        assert.strictEqual(err.error.errorCode.number, 6020);
        assert.strictEqual(err.error.errorCode.code, "UnrealizedReward");
        assert.strictEqual(
          err.error.errorMessage,
          "Locked rewards cannot be realized until one unstaked all tokens."
        );
        expect(err.error.origin).to.deep.equal({
          file: "programs/registry/src/lib.rs",
          line: 63,
        });
        assert.strictEqual(
          err.program.toString(),
          "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L"
        );
        expect(err.programErrorStack.map((pk) => pk.toString())).to.deep.equal([
          "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
          "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L",
        ]);
        return true;
      }
    );
  });

  const pendingWithdrawal = anchor.web3.Keypair.generate();

  it("Unstakes (unlocked)", async () => {
    const unstakeAmount = new anchor.BN(10);

    await registry.rpc.startUnstake(unstakeAmount, false, {
      accounts: {
        registrar: registrar.publicKey,
        rewardEventQ: rewardQ.publicKey,
        poolMint,

        pendingWithdrawal: pendingWithdrawal.publicKey,
        member: member.publicKey,
        beneficiary: provider.wallet.publicKey,
        balances,
        balancesLocked,

        memberSigner,

        tokenProgram: TOKEN_PROGRAM_ID,
        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
      },
      signers: [pendingWithdrawal],
      instructions: [
        await registry.account.pendingWithdrawal.createInstruction(
          pendingWithdrawal
        ),
      ],
    });

    const vaultPw = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.vaultPw
    );
    const vaultStake = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.vaultStake
    );
    const spt = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.spt
    );

    assert.isTrue(vaultPw.amount.eq(new anchor.BN(20)));
    assert.isTrue(vaultStake.amount.eq(new anchor.BN(0)));
    assert.isTrue(spt.amount.eq(new anchor.BN(0)));
  });

  const tryEndUnstake = async () => {
    await registry.rpc.endUnstake({
      accounts: {
        registrar: registrar.publicKey,

        member: member.publicKey,
        beneficiary: provider.wallet.publicKey,
        pendingWithdrawal: pendingWithdrawal.publicKey,

        vault: balances.vault,
        vaultPw: balances.vaultPw,

        memberSigner,

        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
    });
  };

  it("Fails to end unstaking before timelock", async () => {
    await nativeAssert.rejects(
      async () => {
        await tryEndUnstake();
      },
      (err) => {
        assert.strictEqual(err.error.errorCode.number, 6009);
        assert.strictEqual(
          err.error.errorMessage,
          "The unstake timelock has not yet expired."
        );
        return true;
      }
    );
  });

  it("Waits for the unstake period to end", async () => {
    await serumCmn.sleep(5000);
  });

  it("Unstake finalizes (unlocked)", async () => {
    await tryEndUnstake();

    const vault = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.vault
    );
    const vaultPw = await serumCmn.getTokenAccount(
      provider,
      memberAccount.balances.vaultPw
    );

    assert.isTrue(vault.amount.eq(new anchor.BN(120)));
    assert.isTrue(vaultPw.amount.eq(new anchor.BN(0)));
  });

  it("Withdraws deposits (unlocked)", async () => {
    const token = await serumCmn.createTokenAccount(
      provider,
      mint,
      provider.wallet.publicKey
    );
    const withdrawAmount = new anchor.BN(100);
    await registry.rpc.withdraw(withdrawAmount, {
      accounts: {
        registrar: registrar.publicKey,
        member: member.publicKey,
        beneficiary: provider.wallet.publicKey,
        vault: memberAccount.balances.vault,
        memberSigner,
        depositor: token,
        tokenProgram: TOKEN_PROGRAM_ID,
      },
    });

    const tokenAccount = await serumCmn.getTokenAccount(provider, token);
    assert.isTrue(tokenAccount.amount.eq(withdrawAmount));
  });

  it("Should succesfully unlock a locked reward after unstaking", async () => {
    const token = await serumCmn.createTokenAccount(
      provider,
      mint,
      provider.wallet.publicKey
    );

    const withdrawAmount = new anchor.BN(7);
    await lockup.rpc.withdraw(withdrawAmount, {
      accounts: {
        vesting: vendoredVesting.publicKey,
        beneficiary: provider.wallet.publicKey,
        token,
        vault: vendoredVestingVault.publicKey,
        vestingSigner: vendoredVestingSigner,
        tokenProgram: TOKEN_PROGRAM_ID,
        clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
      },
      // TODO: trait methods generated on the client. Until then, we need to manually
      //       specify the account metas here.
      remainingAccounts: [
        { pubkey: registry.programId, isWritable: false, isSigner: false },
        { pubkey: member.publicKey, isWritable: false, isSigner: false },
        { pubkey: balances.spt, isWritable: false, isSigner: false },
        { pubkey: balancesLocked.spt, isWritable: false, isSigner: false },
      ],
    });
    const tokenAccount = await serumCmn.getTokenAccount(provider, token);
    assert.isTrue(tokenAccount.amount.eq(withdrawAmount));
  });
		*/
});
