import { expect } from "chai";
import { testingPage } from "./mocha.global.setup.mjs";
import {
  consumeTransaction,
  mintTransaction,
  setupWalletAndFaucet,
} from "./webClientTestUtils";
import { TransactionProver } from "../dist";
import { setupConsumedNote } from "./notes.test";

// NEW_MINT_TRANSACTION TESTS
// =======================================================================================================

describe("new_mint_transactions tests", () => {
  it("new_mint_transaction completes successfully", async () => {
    const { faucetId, accountId } = await setupWalletAndFaucet();
    const result = await mintTransaction(accountId, faucetId);

    expect(result.transactionId).to.not.be.empty;
    expect(result.numOutputNotesCreated).to.equal(1);
    expect(result.nonce).to.equal("1");
  });
});

// NEW_CONSUME_TRANSACTION TESTS
// =======================================================================================================

describe("new_consume_transaction tests", () => {
  it("new_consume_transaction completes successfully", async () => {
    const { faucetId, accountId } = await setupWalletAndFaucet();
    const { createdNoteId } = await mintTransaction(accountId, faucetId);
    const result = await consumeTransaction(accountId, faucetId, createdNoteId);

    expect(result.transactionId).to.not.be.empty;
    expect(result.nonce).to.equal("1");
    expect(result.numConsumedNotes).to.equal(1);
    expect(result.targetAccountBalanace).to.equal("1000");
  });
});

// NEW_SEND_TRANSACTION TESTS
// =======================================================================================================

interface SendTransactionResult {
  senderAccountBalance: string;
  changedTargetBalance: string;
}

export const sendTransaction = async (): Promise<SendTransactionResult> => {
  return await testingPage.evaluate(async () => {
    const client = window.client;

    const senderAccount = await client.new_wallet(
      window.AccountStorageMode.private(),
      true
    );
    const targetAccount = await client.new_wallet(
      window.AccountStorageMode.private(),
      true
    );
    const faucetAccount = await client.new_faucet(
      window.AccountStorageMode.private(),
      false,
      "DAG",
      8,
      BigInt(10000000)
    );
    await client.sync_state();

    await client.fetch_and_cache_account_auth_by_pub_key(faucetAccount.id());
    let mint_transaction_result = await client.new_mint_transaction(
      senderAccount.id(),
      faucetAccount.id(),
      window.NoteType.private(),
      BigInt(1000)
    );
    let created_notes = mint_transaction_result.created_notes().notes();
    let created_note_ids = created_notes.map((note) => note.id().to_string());
    await window.helpers.waitForTransaction(
      mint_transaction_result.executed_transaction().id().to_hex()
    );

    await client.fetch_and_cache_account_auth_by_pub_key(senderAccount.id());
    const senderConsumeTransactionResult = await client.new_consume_transaction(
      senderAccount.id(),
      created_note_ids
    );
    await window.helpers.waitForTransaction(
      senderConsumeTransactionResult.executed_transaction().id().to_hex()
    );

    await client.fetch_and_cache_account_auth_by_pub_key(senderAccount.id());
    let send_transaction_result = await client.new_send_transaction(
      senderAccount.id(),
      targetAccount.id(),
      faucetAccount.id(),
      window.NoteType.private(),
      BigInt(100)
    );
    let send_created_notes = send_transaction_result.created_notes().notes();
    let send_created_note_ids = send_created_notes.map((note) =>
      note.id().to_string()
    );
    await window.helpers.waitForTransaction(
      send_transaction_result.executed_transaction().id().to_hex()
    );

    await client.fetch_and_cache_account_auth_by_pub_key(targetAccount.id());
    const targetConsumeTransactionResult = await client.new_consume_transaction(
      targetAccount.id(),
      send_created_note_ids
    );
    await window.helpers.waitForTransaction(
      targetConsumeTransactionResult.executed_transaction().id().to_hex()
    );

    const changedSenderAccount = await client.get_account(senderAccount.id());
    const changedTargetAccount = await client.get_account(targetAccount.id());

    return {
      senderAccountBalance: changedSenderAccount
        .vault()
        .get_balance(faucetAccount.id())
        .toString(),
      changedTargetBalance: changedTargetAccount
        .vault()
        .get_balance(faucetAccount.id())
        .toString(),
    };
  });
};

describe("new_send_transaction tests", () => {
  it("new_send_transaction completes successfully", async () => {
    const result = await sendTransaction();

    expect(result.senderAccountBalance).to.equal("900");
    expect(result.changedTargetBalance).to.equal("100");
  });
});

// CUSTOM_TRANSACTIONS TESTS
// =======================================================================================================

export const customTransaction = async (
  asserted_value: string,
  with_custom_prover: boolean
): Promise<void> => {
  return await testingPage.evaluate(
    async (_asserted_value: string, _with_custom_prover: boolean) => {
      const client = window.client;

      const walletAccount = await client.new_wallet(
        window.AccountStorageMode.private(),
        false
      );
      const faucetAccount = await client.new_faucet(
        window.AccountStorageMode.private(),
        false,
        "DAG",
        8,
        BigInt(10000000)
      );
      await client.sync_state();

      // Creating Custom Note which needs the following:
      // - Note Assets
      // - Note Metadata
      // - Note Recipient

      // Creating NOTE_ARGS
      let felt1 = new window.Felt(BigInt(9));
      let felt2 = new window.Felt(BigInt(12));
      let felt3 = new window.Felt(BigInt(18));
      let felt4 = new window.Felt(BigInt(3));
      let felt5 = new window.Felt(BigInt(3));
      let felt6 = new window.Felt(BigInt(18));
      let felt7 = new window.Felt(BigInt(12));
      let felt8 = new window.Felt(BigInt(9));

      let noteArgs = [felt1, felt2, felt3, felt4, felt5, felt6, felt7, felt8];
      let feltArray = new window.FeltArray();
      noteArgs.forEach((felt) => feltArray.append(felt));

      let noteAssets = new window.NoteAssets([
        new window.FungibleAsset(faucetAccount.id(), BigInt(10)),
      ]);

      let noteMetadata = new window.NoteMetadata(
        faucetAccount.id(),
        window.NoteType.private(),
        window.NoteTag.from_account_id(
          walletAccount.id(),
          window.NoteExecutionMode.new_local()
        ),
        window.NoteExecutionHint.none(),
        undefined
      );

      let expectedNoteArgs = noteArgs.map((felt) => felt.as_int());
      let memAddress = "1000";
      let memAddress2 = "1004";
      let expectedNoteArg1 = expectedNoteArgs.slice(0, 4).join(".");
      let expectedNoteArg2 = expectedNoteArgs.slice(4, 8).join(".");
      let note_script = `
            # Custom P2ID note script
            #
            # This note script asserts that the note args are exactly the same as passed 
            # (currently defined as {expected_note_arg_1} and {expected_note_arg_2}).
            # Since the args are too big to fit in a single note arg, we provide them via advice inputs and 
            # address them via their commitment (noted as NOTE_ARG)
            # This note script is based off of the P2ID note script because notes currently need to have 
            # assets, otherwise it could have been boiled down to the assert. 

            use.miden::account
            use.miden::note
            use.miden::contracts::wallets::basic->wallet
            use.std::mem


            proc.add_note_assets_to_account
                push.0 exec.note::get_assets
                # => [num_of_assets, 0 = ptr, ...]

                # compute the pointer at which we should stop iterating
                mul.4 dup.1 add
                # => [end_ptr, ptr, ...]

                # pad the stack and move the pointer to the top
                padw movup.5
                # => [ptr, 0, 0, 0, 0, end_ptr, ...]

                # compute the loop latch
                dup dup.6 neq
                # => [latch, ptr, 0, 0, 0, 0, end_ptr, ...]

                while.true
                    # => [ptr, 0, 0, 0, 0, end_ptr, ...]

                    # save the pointer so that we can use it later
                    dup movdn.5
                    # => [ptr, 0, 0, 0, 0, ptr, end_ptr, ...]

                    # load the asset
                    mem_loadw
                    # => [ASSET, ptr, end_ptr, ...]

                    # pad the stack before call
                    padw swapw padw padw swapdw
                    # => [ASSET, pad(12), ptr, end_ptr, ...]

                    # add asset to the account
                    call.wallet::receive_asset
                    # => [pad(16), ptr, end_ptr, ...]

                    # clean the stack after call
                    dropw dropw dropw
                    # => [0, 0, 0, 0, ptr, end_ptr, ...]

                    # increment the pointer and compare it to the end_ptr
                    movup.4 add.4 dup dup.6 neq
                    # => [latch, ptr+4, ASSET, end_ptr, ...]
                end

                # clear the stack
                drop dropw drop
            end

            begin
                # push data from the advice map into the advice stack
                adv.push_mapval
                # => [NOTE_ARG] 

                # memory address where to write the data
                push.${memAddress}
                # => [target_mem_addr, NOTE_ARG_COMMITMENT]
                # number of words
                push.2
                # => [number_of_words, target_mem_addr, NOTE_ARG_COMMITMENT]
                exec.mem::pipe_preimage_to_memory
                # => [target_mem_addr']
                dropw
                # => []
                
                # read first word
                push.${memAddress}
                # => [data_mem_address]
                mem_loadw
                # => [NOTE_ARG_1]
                
                push.${expectedNoteArg1} assert_eqw.err=101
                # => []

                # read second word
                push.${memAddress2}
                # => [data_mem_address_2]
                mem_loadw
                # => [NOTE_ARG_2]

                push.${expectedNoteArg2} assert_eqw.err=102
                # => []

                # store the note inputs to memory starting at address 0
                push.0 exec.note::get_inputs
                # => [num_inputs, inputs_ptr]

                # make sure the number of inputs is 1
                eq.2 assert.err=103
                # => [inputs_ptr]

                # read the target account id from the note inputs
                mem_load
                # => [target_account_id_prefix]

                exec.account::get_id swap drop
                # => [account_id_prefix, target_account_id_prefix, ...]

                # ensure account_id = target_account_id, fails otherwise
                assert_eq.err=104
                # => [...]

                exec.add_note_assets_to_account
                # => [...]
            end
        `;

      let compiledNoteScript = await client.compile_note_script(note_script);
      let noteInputs = new window.NoteInputs(
        new window.FeltArray([
          walletAccount.id().prefix(),
          walletAccount.id().suffix(),
        ])
      );

      const serialNum = window.Word.new_from_u64s(
        new BigUint64Array([BigInt(1), BigInt(2), BigInt(3), BigInt(4)])
      );
      let noteRecipient = new window.NoteRecipient(
        serialNum,
        compiledNoteScript,
        noteInputs
      );

      let note = new window.Note(noteAssets, noteMetadata, noteRecipient);

      // Creating First Custom Transaction Request to Mint the Custom Note
      let transaction_request = new window.TransactionRequestBuilder()
        .with_own_output_notes(
          new window.OutputNotesArray([window.OutputNote.full(note)])
        )
        .build();

      // Execute and Submit Transaction
      await client.fetch_and_cache_account_auth_by_pub_key(faucetAccount.id());
      let transaction_result = await client.new_transaction(
        faucetAccount.id(),
        transaction_request
      );

      if (_with_custom_prover) {
        await client.submit_transaction(
          transaction_result,
          await selectProver()
        );
      } else {
        await client.submit_transaction(transaction_result);
      }

      await window.helpers.waitForTransaction(
        transaction_result.executed_transaction().id().to_hex()
      );

      // Just like in the miden test, you can modify this script to get the execution to fail
      // by modifying the assert
      let tx_script = `
            use.miden::contracts::auth::basic->auth_tx
            use.miden::kernels::tx::prologue
            use.miden::kernels::tx::memory

            begin
                push.0 push.${_asserted_value}
                # => [0, ${_asserted_value}]
                assert_eq

                call.auth_tx::auth_tx_rpo_falcon512
            end
        `;

      // Creating Second Custom Transaction Request to Consume Custom Note
      // with Invalid/Valid Transaction Script
      let account_auth = await client.get_account_auth(walletAccount.id());
      let public_key = account_auth.get_rpo_falcon_512_public_key_as_word();
      let secret_key = account_auth.get_rpo_falcon_512_secret_key_as_felts();
      let transcription_script_input_pair_array =
        new window.TransactionScriptInputPairArray([
          new window.TransactionScriptInputPair(public_key, secret_key),
        ]);
      let transaction_script = await client.compile_tx_script(
        tx_script,
        transcription_script_input_pair_array
      );
      let note_id = note.id();
      let note_args_commitment = window.Rpo256.hash_elements(feltArray); // gets consumed by NoteIdAndArgs
      let note_id_and_args = new window.NoteIdAndArgs(
        note_id,
        note_args_commitment.to_word()
      );
      let note_id_and_args_array = new window.NoteIdAndArgsArray([
        note_id_and_args,
      ]);
      let advice_map = new window.AdviceMap();
      let note_args_commitment_2 = window.Rpo256.hash_elements(feltArray);
      advice_map.insert(note_args_commitment_2, feltArray);

      let transaction_request_2 = new window.TransactionRequestBuilder()
        .with_authenticated_input_notes(note_id_and_args_array)
        .with_custom_script(transaction_script)
        .extend_advice_map(advice_map)
        .build();

      // Execute and Submit Transaction
      await client.fetch_and_cache_account_auth_by_pub_key(walletAccount.id());
      let transaction_result_2 = await client.new_transaction(
        walletAccount.id(),
        transaction_request_2
      );

      if (_with_custom_prover) {
        await client.submit_transaction(
          transaction_result_2,
          await selectProver()
        );
      } else {
        await client.submit_transaction(transaction_result_2);
      }

      await window.helpers.waitForTransaction(
        transaction_result_2.executed_transaction().id().to_hex()
      );
    },
    asserted_value,
    with_custom_prover
  );
};

const customTxWithMultipleNotes = async (
  isSerialNumSame: boolean,
  senderAccountId: string,
  faucetAccountId: string
) => {
  return await testingPage.evaluate(
    async (_isSerialNumSame, _senderAccountId, _faucetAccountId) => {
      const client = window.client;
      const amount = BigInt(10);
      const targetAccount = await client.new_wallet(
        window.AccountStorageMode.private(),
        true
      );
      const targetAccountId = targetAccount.id();
      const senderAccountId = window.AccountId.from_hex(_senderAccountId);
      const faucetAccountId = window.AccountId.from_hex(_faucetAccountId);

      // Create custom note with multiple assets to send to target account
      // Error should happen if serial numbers are the same in each set of
      // note assets. Otherwise, the transaction should go through.
      await client.fetch_and_cache_account_auth_by_pub_key(senderAccountId);

      let noteAssets_1 = new window.NoteAssets([
        new window.FungibleAsset(faucetAccountId, amount),
      ]);
      let noteAssets_2 = new window.NoteAssets([
        new window.FungibleAsset(faucetAccountId, amount),
      ]);

      let noteMetadata = new window.NoteMetadata(
        senderAccountId,
        window.NoteType.public(),
        window.NoteTag.from_account_id(
          targetAccountId,
          window.NoteExecutionMode.new_local()
        ),
        window.NoteExecutionHint.none(),
        undefined
      );

      let serialNum1 = window.Word.new_from_u64s(
        new BigUint64Array([BigInt(1), BigInt(2), BigInt(3), BigInt(4)])
      );
      let serialNum2 = window.Word.new_from_u64s(
        new BigUint64Array([BigInt(5), BigInt(6), BigInt(7), BigInt(8)])
      );

      const p2idScript = window.NoteScript.p2id();

      let noteInputs = new window.NoteInputs(
        new window.FeltArray([
          targetAccount.id().suffix(),
          targetAccount.id().prefix(),
        ])
      );

      let noteRecipient1 = new window.NoteRecipient(
        serialNum1,
        p2idScript,
        noteInputs
      );
      let noteRecipient2 = new window.NoteRecipient(
        _isSerialNumSame ? serialNum1 : serialNum2,
        p2idScript,
        noteInputs
      );

      let note1 = new window.Note(noteAssets_1, noteMetadata, noteRecipient1);
      let note2 = new window.Note(noteAssets_2, noteMetadata, noteRecipient2);

      let transaction_request = new window.TransactionRequestBuilder()
        .with_own_output_notes(
          new window.OutputNotesArray([
            window.OutputNote.full(note1),
            window.OutputNote.full(note2),
          ])
        )
        .build();

      let transactionResult = await client.new_transaction(
        senderAccountId,
        transaction_request
      );

      await client.submit_transaction(transactionResult);

      await window.helpers.waitForTransaction(
        transactionResult.executed_transaction().id().to_hex()
      );
    },
    isSerialNumSame,
    senderAccountId,
    faucetAccountId
  );
};

describe("custom transaction tests", () => {
  it("custom transaction completes successfully", async () => {
    await expect(customTransaction("0", false)).to.be.fulfilled;
  });

  it("custom transaction fails", async () => {
    await expect(customTransaction("1", false)).to.be.rejected;
  });
});

describe("custom transaction with multiple output notes", () => {
  const testCases = [
    {
      description: "does not fail when output note serial numbers are unique",
      shouldFail: false,
    },
    {
      description: "fails when output note serial numbers are the same",
      shouldFail: true,
    },
  ];

  testCases.forEach(({ description, shouldFail }) => {
    it(description, async () => {
      const { accountId, faucetId } = await setupConsumedNote();
      if (shouldFail) {
        await expect(customTxWithMultipleNotes(shouldFail, accountId, faucetId))
          .to.be.rejected;
      } else {
        await expect(customTxWithMultipleNotes(shouldFail, accountId, faucetId))
          .to.be.fulfilled;
      }
    });
  });
});

// CUSTOM PROVERS TEST
// ================================================================================================

export const selectProver = async (): Promise<TransactionProver> => {
  if (window.remote_prover_url != null) {
    return window.TransactionProver.new_remote_prover(window.remote_prover_url);
  } else {
    return window.TransactionProver.new_local_prover();
  }
};

describe("use custom transaction prover per request", () => {
  it("custom transaction prover completes successfully"),
    async () => {
      await expect(customTransaction("0", true)).to.be.fulfilled;
    };
});