import * as anchor from "@coral-xyz/anchor";
import { Native } from "@coral-xyz/anchor";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
  Keypair,
  LAMPORTS_PER_SOL,
  NONCE_ACCOUNT_LENGTH,
  PublicKey,
  SystemProgram,
  SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
} from "@solana/web3.js";
import * as assert from "assert";
import BN from "bn.js";

describe("system-coder", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  // Client.
  const program = Native.system();

  // Constants.
  const aliceKeypair = Keypair.generate();

  it("Creates an account", async () => {
    // arrange
    const space = 100;
    const lamports =
      await program.provider.connection.getMinimumBalanceForRentExemption(
        space
      );
    const owner = SystemProgram.programId;
    // act
    await program.methods
      .createAccount(new BN(lamports), new BN(space), owner)
      .accounts({
        from: provider.wallet.publicKey,
        to: aliceKeypair.publicKey,
      })
      .signers([aliceKeypair])
      .rpc();
    // assert
    const aliceAccount = await program.provider.connection.getAccountInfo(
      aliceKeypair.publicKey
    );
    assert.notEqual(aliceAccount, null);
    assert.ok(owner.equals(aliceAccount.owner));
    assert.equal(lamports, aliceAccount.lamports);
  });

  it("Assigns an account to a program", async () => {
    // arrange
    const owner = TOKEN_PROGRAM_ID;
    // act
    await program.methods
      .assign(owner)
      .accounts({
        pubkey: aliceKeypair.publicKey,
      })
      .signers([aliceKeypair])
      .rpc();
    // assert
    const aliceAccount = await program.provider.connection.getAccountInfo(
      aliceKeypair.publicKey
    );
    assert.notEqual(aliceAccount, null);
    assert.ok(owner.equals(aliceAccount.owner));
  });

  it("Allocates space to an account", async () => {
    // arrange
    const newKeypair = Keypair.generate();
    const space = 100;
    const lamports =
      await program.provider.connection.getMinimumBalanceForRentExemption(
        space
      );
    // act
    await program.methods
      .allocate(new BN(space))
      .accounts({
        pubkey: newKeypair.publicKey,
      })
      .postInstructions([
        await program.methods
          .transfer(new BN(lamports))
          .accounts({
            from: provider.wallet.publicKey,
            to: newKeypair.publicKey,
          })
          .instruction(),
      ])
      .signers([newKeypair])
      .rpc();
    // assert
    const newAccountAfter = await program.provider.connection.getAccountInfo(
      newKeypair.publicKey
    );
    assert.equal(space, newAccountAfter.data.byteLength);
  });

  it("Creates an account with seed", async () => {
    const space = 100;
    const lamports =
      await program.provider.connection.getMinimumBalanceForRentExemption(
        space
      );
    const owner = SystemProgram.programId;
    const seed = "seeds";
    const bobPublicKey = await PublicKey.createWithSeed(
      aliceKeypair.publicKey,
      seed,
      owner
    );
    // act
    await program.methods
      .createAccountWithSeed(
        aliceKeypair.publicKey,
        seed,
        new BN(lamports),
        new BN(space),
        owner
      )
      .accounts({
        base: aliceKeypair.publicKey,
        from: provider.wallet.publicKey,
        to: bobPublicKey,
      })
      .signers([aliceKeypair])
      .rpc();
    // assert
    const bobAccount = await program.provider.connection.getAccountInfo(
      bobPublicKey
    );
    assert.notEqual(bobAccount, null);
  });

  it("Allocates and assigns an account with seed", async () => {
    const owner = TOKEN_PROGRAM_ID;
    const seed = "seeds2";
    const space = 100;
    const lamports =
      await program.provider.connection.getMinimumBalanceForRentExemption(
        space
      );
    const bobPublicKey = await PublicKey.createWithSeed(
      aliceKeypair.publicKey,
      seed,
      owner
    );
    // act
    await program.methods
      .allocateWithSeed(aliceKeypair.publicKey, seed, new BN(space), owner)
      .accounts({
        base: aliceKeypair.publicKey,
        account: bobPublicKey,
      })
      .postInstructions([
        await program.methods
          .transfer(new BN(lamports))
          .accounts({
            from: provider.wallet.publicKey,
            to: bobPublicKey,
          })
          .instruction(),
        await program.methods
          .assignWithSeed(aliceKeypair.publicKey, seed, owner)
          .accounts({
            base: aliceKeypair.publicKey,
            account: bobPublicKey,
          })
          .instruction(),
      ])
      .signers([aliceKeypair])
      .rpc();
    // assert
    const bobAccount = await program.provider.connection.getAccountInfo(
      bobPublicKey
    );
    assert.notEqual(bobAccount, null);
    assert.ok(owner.equals(bobAccount.owner));
  });

  it("Transfers from account with seed", async () => {
    const lamports = 1 * LAMPORTS_PER_SOL;
    const owner = SystemProgram.programId;
    const seed = "seeds3";
    const bobPublicKey = await PublicKey.createWithSeed(
      aliceKeypair.publicKey,
      seed,
      owner
    );
    const aliceAccountBefore = await program.provider.connection.getAccountInfo(
      aliceKeypair.publicKey
    );
    // act
    await program.methods
      .transfer(new BN(lamports))
      .accounts({
        from: provider.wallet.publicKey,
        to: bobPublicKey,
      })
      .rpc();
    await program.methods
      .transferWithSeed(new BN(lamports), seed, owner)
      .accounts({
        from: bobPublicKey,
        base: aliceKeypair.publicKey,
        to: aliceKeypair.publicKey,
      })
      .signers([aliceKeypair])
      .rpc();
    // assert
    const aliceAccountAfter = await program.provider.connection.getAccountInfo(
      aliceKeypair.publicKey
    );
    assert.equal(
      aliceAccountBefore.lamports + lamports,
      aliceAccountAfter.lamports
    );
  });

  it("Transfers lamports", async () => {
    // arrange
    const receiverKeypair = Keypair.generate();
    const lamports = 0.1 * LAMPORTS_PER_SOL;
    // act
    await program.methods
      .transfer(new BN(lamports))
      .accounts({
        from: provider.wallet.publicKey,
        to: receiverKeypair.publicKey,
      })
      .rpc();
    // assert
    const receiverAccount = await program.provider.connection.getAccountInfo(
      receiverKeypair.publicKey
    );
    assert.notEqual(receiverAccount, null);
    assert.equal(lamports, receiverAccount.lamports);
  });

  it("Initializes nonce account", async () => {
    // arrange
    const nonceKeypair = Keypair.generate();
    const owner = SystemProgram.programId;
    const space = NONCE_ACCOUNT_LENGTH;
    const lamports =
      await provider.connection.getMinimumBalanceForRentExemption(space);
    // act
    await program.methods
      .initializeNonceAccount(provider.wallet.publicKey)
      .accounts({
        nonce: nonceKeypair.publicKey,
        recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
      })
      .preInstructions([
        await program.methods
          .createAccount(new BN(lamports), new BN(space), owner)
          .accounts({
            from: provider.wallet.publicKey,
            to: nonceKeypair.publicKey,
          })
          .instruction(),
      ])
      .signers([nonceKeypair])
      .rpc();
    // assert
    const nonceAccount = await program.account.nonce.fetch(
      nonceKeypair.publicKey
    );
    assert.notEqual(nonceAccount, null);
    assert.ok(nonceAccount.authorizedPubkey.equals(provider.wallet.publicKey));
  });

  it("Advances a nonce account", async () => {
    // arrange
    const nonceKeypair = Keypair.generate();
    const owner = SystemProgram.programId;
    const space = NONCE_ACCOUNT_LENGTH;
    const lamports =
      await provider.connection.getMinimumBalanceForRentExemption(space);
    // act
    await program.methods
      .initializeNonceAccount(provider.wallet.publicKey)
      .accounts({
        nonce: nonceKeypair.publicKey,
        recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
      })
      .preInstructions([
        await program.methods
          .createAccount(new BN(lamports), new BN(space), owner)
          .accounts({
            from: provider.wallet.publicKey,
            to: nonceKeypair.publicKey,
          })
          .instruction(),
      ])
      .signers([nonceKeypair])
      .rpc();
    // These have to be separate to make sure advance is in another slot.
    await program.methods
      .advanceNonceAccount(provider.wallet.publicKey)
      .accounts({
        nonce: nonceKeypair.publicKey,
        recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
      })
      .rpc();
    // assert
    const nonceAccount = await program.account.nonce.fetch(
      nonceKeypair.publicKey
    );
    assert.notEqual(nonceAccount, null);
  });

  it("Authorizes a nonce account", async () => {
    // arrange
    const nonceKeypair = Keypair.generate();
    const owner = SystemProgram.programId;
    const space = NONCE_ACCOUNT_LENGTH;
    const lamports =
      await provider.connection.getMinimumBalanceForRentExemption(space);
    // act
    await program.methods
      .initializeNonceAccount(provider.wallet.publicKey)
      .accounts({
        nonce: nonceKeypair.publicKey,
        recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
      })
      .preInstructions([
        await program.methods
          .createAccount(new BN(lamports), new BN(space), owner)
          .accounts({
            from: provider.wallet.publicKey,
            to: nonceKeypair.publicKey,
          })
          .instruction(),
      ])
      .signers([nonceKeypair])
      .rpc();
    await program.methods
      .authorizeNonceAccount(aliceKeypair.publicKey)
      .accounts({
        nonce: nonceKeypair.publicKey,
        authorized: provider.wallet.publicKey,
      })
      .rpc();
    // assert
    const nonceAccount = await program.account.nonce.fetch(
      nonceKeypair.publicKey
    );
    assert.notEqual(nonceAccount, null);
    assert.ok(nonceAccount.authorizedPubkey.equals(aliceKeypair.publicKey));
  });

  it("Withdraws from nonce account", async () => {
    // arrange
    const nonceKeypair = Keypair.generate();
    const owner = SystemProgram.programId;
    const space = NONCE_ACCOUNT_LENGTH;
    const lamports =
      await provider.connection.getMinimumBalanceForRentExemption(space);
    const amount = 0.1 * LAMPORTS_PER_SOL;
    const aliceBalanceBefore = (
      await program.provider.connection.getAccountInfo(aliceKeypair.publicKey)
    ).lamports;
    // act
    await program.methods
      .initializeNonceAccount(provider.wallet.publicKey)
      .accounts({
        nonce: nonceKeypair.publicKey,
        recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
      })
      .preInstructions([
        await program.methods
          .createAccount(new BN(lamports), new BN(space), owner)
          .accounts({
            from: provider.wallet.publicKey,
            to: nonceKeypair.publicKey,
          })
          .instruction(),
      ])
      .signers([nonceKeypair])
      .rpc();
    await program.methods
      .advanceNonceAccount(provider.wallet.publicKey)
      .accounts({
        nonce: nonceKeypair.publicKey,
        recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
      })
      .postInstructions([
        await program.methods
          .transfer(new BN(amount))
          .accounts({
            from: provider.wallet.publicKey,
            to: nonceKeypair.publicKey,
          })
          .instruction(),
      ])
      .rpc();
    await program.methods
      .authorizeNonceAccount(aliceKeypair.publicKey)
      .accounts({
        nonce: nonceKeypair.publicKey,
        authorized: provider.wallet.publicKey,
      })
      .rpc();
    await program.methods
      .withdrawNonceAccount(new BN(amount))
      .accounts({
        authorized: aliceKeypair.publicKey,
        nonce: nonceKeypair.publicKey,
        recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
        to: aliceKeypair.publicKey,
      })
      .signers([aliceKeypair])
      .rpc();
    // assert
    const aliceBalanceAfter = (
      await program.provider.connection.getAccountInfo(aliceKeypair.publicKey)
    ).lamports;
    assert.equal(aliceBalanceAfter - aliceBalanceBefore, amount);
  });
});
