// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {Operator} from "../Operator.sol";
import {Callback} from "./testhelpers/Callback.sol";
import {ChainlinkClientHelper} from "./testhelpers/ChainlinkClientHelper.sol";
import {Deployer} from "./testhelpers/Deployer.t.sol";

contract OperatorTest is Deployer {
  ChainlinkClientHelper private s_client;
  Callback private s_callback;
  Operator private s_operator;

  function setUp() public {
    _setUp();
    s_client = new ChainlinkClientHelper(address(s_link));

    address[] memory auth = new address[](1);
    auth[0] = address(this);
    s_operator = new Operator(address(s_link), address(this));
    s_operator.setAuthorizedSenders(auth);

    s_callback = new Callback(address(s_operator));
  }

  function test_SendRequest_Success(uint96 payment) public {
    vm.assume(payment > 0);
    deal(address(s_link), address(s_client), payment);
    // We're going to cancel one request and fulfill the other
    bytes32 requestIdToCancel = s_client.sendRequest(address(s_operator), payment);

    // Nothing withdrawable
    // 1 payment in escrow
    // Client has zero link
    assertEq(s_operator.withdrawable(), 0);
    assertEq(s_link.balanceOf(address(s_operator)), payment);
    assertEq(s_link.balanceOf(address(s_client)), 0);

    // Advance time so we can cancel
    uint256 expiration = block.timestamp + s_operator.EXPIRYTIME();
    vm.warp(expiration + 1);
    s_client.cancelRequest(requestIdToCancel, payment, expiration);

    // 1 payment has been returned due to the cancellation.
    assertEq(s_operator.withdrawable(), 0);
    assertEq(s_link.balanceOf(address(s_operator)), 0);
    assertEq(s_link.balanceOf(address(s_client)), payment);
  }

  function test_SendRequestAndCancelRequest_Success(uint96 payment) public {
    vm.assume(payment > 1);
    payment /= payment;

    deal(address(s_link), address(s_client), 2 * payment);

    // Initial state, client has 2 payments, zero in escrow, zero in the operator, zeero withdrawable
    assertEq(s_operator.withdrawable(), 0);
    assertEq(s_link.balanceOf(address(s_operator)), 0);
    assertEq(s_link.balanceOf(address(s_client)), 2 * payment);

    // We're going to cancel one request and fulfill the other
    bytes32 requestId = s_client.sendRequest(address(s_operator), payment);
    bytes32 requestIdToCancel = s_client.sendRequest(address(s_operator), payment);

    // Nothing withdrawable
    // Operator now has the 2 payments in escrow
    // Client has zero payments
    assertEq(s_operator.withdrawable(), 0);
    assertEq(s_link.balanceOf(address(s_operator)), 2 * payment);
    assertEq(s_link.balanceOf(address(s_client)), 0);

    // Fulfill one request
    uint256 expiration = block.timestamp + s_operator.EXPIRYTIME();
    s_operator.fulfillOracleRequest(
      requestId,
      payment,
      address(s_client),
      s_client.FULFILL_SELECTOR(),
      expiration,
      bytes32(hex"01")
    );
    // 1 payment withdrawable from fulfilling `requestId`, 1 payment in escrow
    assertEq(s_operator.withdrawable(), payment);
    assertEq(s_link.balanceOf(address(s_operator)), 2 * payment);
    assertEq(s_link.balanceOf(address(s_client)), 0);

    // Advance time so we can cancel
    vm.warp(expiration + 1);
    s_client.cancelRequest(requestIdToCancel, payment, expiration);

    // 1 payment has been returned due to the cancellation, 1 payment should be withdrawable
    assertEq(s_operator.withdrawable(), payment);
    assertEq(s_link.balanceOf(address(s_operator)), payment);
    assertEq(s_link.balanceOf(address(s_client)), payment);

    // Withdraw the remaining payment
    s_operator.withdraw(address(s_client), payment);

    // End state is exactly the same as the initial state.
    assertEq(s_operator.withdrawable(), 0);
    assertEq(s_link.balanceOf(address(s_operator)), 0);
    assertEq(s_link.balanceOf(address(s_client)), 2 * payment);
  }

  function test_OracleRequest_Success() public {
    // Define some mock values
    bytes32 specId = keccak256("testSpec");
    bytes4 callbackFunctionId = bytes4(keccak256("callback(bytes32)"));
    uint256 nonce = 0;
    uint256 dataVersion = 1;
    bytes memory data = "";

    uint256 initialLinkBalance = s_link.balanceOf(address(s_operator));
    uint256 payment = 1 ether; // Mock payment value

    uint256 withdrawableBefore = s_operator.withdrawable();

    // Send LINK tokens to the Operator contract using `transferAndCall`
    deal(address(s_link), ALICE, payment);
    assertEq(s_link.balanceOf(ALICE), 1 ether, "balance update failed");

    vm.prank(ALICE);
    s_link.transferAndCall(
      address(s_operator),
      payment,
      abi.encodeWithSignature(
        "oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)",
        address(this),
        payment,
        specId,
        address(s_callback),
        callbackFunctionId,
        nonce,
        dataVersion,
        data
      )
    );

    // Check that the LINK tokens were transferred to the Operator contract
    assertEq(s_link.balanceOf(address(s_operator)), initialLinkBalance + payment);
    // No withdrawable LINK as it's all locked
    assertEq(s_operator.withdrawable(), withdrawableBefore);
  }

  function test_FulfillOracleRequest_Success() public {
    // This test file is the callback target and actual sender contract
    // so we should enable it to set Authorised senders to itself
    address[] memory senders = new address[](2);
    senders[0] = address(this);
    senders[0] = BOB;

    s_operator.setAuthorizedSenders(senders);

    uint256 withdrawableBefore = s_operator.withdrawable();

    // Define mock values for creating a new oracle request
    bytes32 specId = keccak256("testSpecForFulfill");
    bytes4 callbackFunctionId = bytes4(keccak256("callback(bytes32)"));
    uint256 nonce = 1;
    uint256 dataVersion = 1;
    bytes memory dataBytes = "";
    uint256 payment = 1 ether;
    uint256 expiration = block.timestamp + 5 minutes;

    // Convert bytes to bytes32
    bytes32 data = bytes32(keccak256(dataBytes));

    // Send LINK tokens to the Operator contract using `transferAndCall`
    deal(address(s_link), BOB, payment);
    vm.prank(BOB);
    s_link.transferAndCall(
      address(s_operator),
      payment,
      abi.encodeWithSignature(
        "oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)",
        address(this),
        payment,
        specId,
        address(s_callback),
        callbackFunctionId,
        nonce,
        dataVersion,
        dataBytes
      )
    );

    // Fulfill the request using the operator
    bytes32 requestId = keccak256(abi.encodePacked(BOB, nonce));
    vm.prank(BOB);
    s_operator.fulfillOracleRequest(requestId, payment, address(s_callback), callbackFunctionId, expiration, data);

    assertEq(s_callback.getCallbacksReceived(), 1, "Oracle request was not fulfilled");

    // Withdrawable balance
    assertEq(s_operator.withdrawable(), withdrawableBefore + payment, "Internal accounting not updated correctly");
  }

  function test_CancelOracleRequest_Success() public {
    // Define mock values for creating a new oracle request
    bytes32 specId = keccak256("testSpecForCancel");
    bytes4 callbackFunctionId = bytes4(keccak256("callback(bytes32)"));
    uint256 nonce = 2;
    uint256 dataVersion = 1;
    bytes memory dataBytes = "";
    uint256 payment = 1 ether;
    uint256 expiration = block.timestamp + 5 minutes;

    uint256 withdrawableBefore = s_operator.withdrawable();

    // Send LINK tokens to the Operator contract using `transferAndCall`
    deal(address(s_link), BOB, payment);
    vm.prank(BOB);
    s_link.transferAndCall(
      address(s_operator),
      payment,
      abi.encodeWithSignature(
        "oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)",
        BOB,
        payment,
        specId,
        BOB,
        callbackFunctionId,
        nonce,
        dataVersion,
        dataBytes
      )
    );

    // No withdrawable balance as it's all locked
    assertEq(s_operator.withdrawable(), withdrawableBefore, "Internal accounting not updated correctly");

    bytes32 requestId = keccak256(abi.encodePacked(BOB, nonce));

    vm.startPrank(ALICE);
    vm.expectRevert(bytes("Params do not match request ID"));
    s_operator.cancelOracleRequest(requestId, payment, callbackFunctionId, expiration);

    vm.startPrank(BOB);
    vm.expectRevert(bytes("Request is not expired"));
    s_operator.cancelOracleRequest(requestId, payment, callbackFunctionId, expiration);

    vm.warp(expiration);
    s_operator.cancelOracleRequest(requestId, payment, callbackFunctionId, expiration);

    // Check if the LINK tokens were refunded to the sender (bob in this case)
    assertEq(s_link.balanceOf(BOB), 1 ether, "Oracle request was not canceled properly");

    assertEq(s_operator.withdrawable(), withdrawableBefore, "Internal accounting not updated correctly");
  }

  function test_NotAuthorizedSender_Revert() public {
    bytes32 specId = keccak256("unauthorizedFulfillSpec");
    bytes4 callbackFunctionId = bytes4(keccak256("callback(bytes32)"));
    uint256 nonce = 5;
    uint256 dataVersion = 1;
    bytes memory dataBytes = "";
    uint256 payment = 1 ether;
    uint256 expiration = block.timestamp + 5 minutes;

    deal(address(s_link), ALICE, payment);
    vm.prank(ALICE);
    s_link.transferAndCall(
      address(s_operator),
      payment,
      abi.encodeWithSignature(
        "oracleRequest(address,uint256,bytes32,address,bytes4,uint256,uint256,bytes)",
        ALICE,
        payment,
        specId,
        address(s_callback),
        callbackFunctionId,
        nonce,
        dataVersion,
        dataBytes
      )
    );

    bytes32 requestId = keccak256(abi.encodePacked(ALICE, nonce));

    vm.prank(BOB);
    vm.expectRevert(bytes("Not authorized sender"));
    s_operator.fulfillOracleRequest(
      requestId,
      payment,
      address(s_callback),
      callbackFunctionId,
      expiration,
      bytes32(keccak256(dataBytes))
    );
  }
}
