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

import {Test} from "forge-std/Test.sol";
import {PaymentChannelManager} from "../../contracts/channels/PaymentChannelManager.sol";
import {IPaymentChannelManager} from "../../contracts/channels/IPaymentChannelManager.sol";

contract PaymentChannelManagerTest is Test {
    PaymentChannelManager public manager;

    address public admin;
    address public alice;
    address public bob;
    uint256 public alicePk;
    uint256 public bobPk;

    uint256 constant CHALLENGE_WINDOW = 1 hours;

    function setUp() public {
        admin = address(0x1);
        (alice, alicePk) = makeAddrAndKey("alice");
        (bob, bobPk) = makeAddrAndKey("bob");
        manager = new PaymentChannelManager(admin, CHALLENGE_WINDOW);
        vm.deal(alice, 100 ether);
        vm.deal(bob, 100 ether);
    }

    function _signState(
        uint256 channelId,
        uint256 nonce,
        uint256 balanceA,
        uint256 balanceB,
        uint256 pk
    ) internal view returns (uint8 v, bytes32 r, bytes32 s) {
        bytes32 stateHash = keccak256(abi.encodePacked(channelId, nonce, balanceA, balanceB));
        bytes32 ethSigned = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", stateHash));
        (v, r, s) = vm.sign(pk, ethSigned);
    }

    function test_openChannel() public {
        vm.prank(alice);
        uint256 channelId = manager.openChannel{value: 10 ether}(bob);
        assertEq(channelId, 0);

        IPaymentChannelManager.Channel memory ch = manager.getChannel(channelId);
        assertEq(ch.participantA, alice);
        assertEq(ch.participantB, bob);
        assertEq(ch.depositA, 10 ether);
        assertEq(ch.depositB, 0);
        assertEq(uint256(ch.status), uint256(IPaymentChannelManager.ChannelStatus.Open));
        assertEq(address(manager).balance, 10 ether);
    }

    function test_openChannel_revertsZeroParticipant() public {
        vm.prank(alice);
        vm.expectRevert("zero participant");
        manager.openChannel(address(0));
    }

    function test_openChannel_revertsZeroDeposit() public {
        vm.prank(alice);
        vm.expectRevert("zero deposit");
        manager.openChannel(bob);
    }

    function test_fundChannel() public {
        vm.prank(alice);
        uint256 channelId = manager.openChannel{value: 10 ether}(bob);
        vm.prank(bob);
        manager.fundChannel{value: 10 ether}(channelId);
        IPaymentChannelManager.Channel memory ch = manager.getChannel(channelId);
        assertEq(ch.depositA, 10 ether);
        assertEq(ch.depositB, 10 ether);
        assertEq(address(manager).balance, 20 ether);
    }

    function test_closeChannelCooperative() public {
        vm.prank(alice);
        uint256 channelId = manager.openChannel{value: 10 ether}(bob);
        vm.prank(bob);
        manager.fundChannel{value: 10 ether}(channelId);

        uint256 balanceA = 15 ether;
        uint256 balanceB = 5 ether;
        uint256 nonce = 1;

        (uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, nonce, balanceA, balanceB, alicePk);
        (uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, nonce, balanceA, balanceB, bobPk);

        uint256 aliceBefore = alice.balance;
        uint256 bobBefore = bob.balance;

        vm.prank(alice);
        manager.closeChannelCooperative(channelId, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB);

        assertEq(alice.balance, aliceBefore + balanceA);
        assertEq(bob.balance, bobBefore + balanceB);
        assertEq(uint256(manager.getChannel(channelId).status), uint256(IPaymentChannelManager.ChannelStatus.Closed));
    }

    function test_submitCloseAndFinalize() public {
        vm.prank(alice);
        uint256 channelId = manager.openChannel{value: 10 ether}(bob);
        vm.prank(bob);
        manager.fundChannel{value: 10 ether}(channelId);

        uint256 balanceA = 12 ether;
        uint256 balanceB = 8 ether;
        uint256 nonce = 1;

        (uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, nonce, balanceA, balanceB, alicePk);
        (uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, nonce, balanceA, balanceB, bobPk);

        vm.prank(alice);
        manager.submitClose(channelId, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB);

        IPaymentChannelManager.Channel memory ch = manager.getChannel(channelId);
        assertEq(uint256(ch.status), uint256(IPaymentChannelManager.ChannelStatus.Dispute));
        assertEq(ch.disputeNonce, nonce);
        assertEq(ch.disputeBalanceA, balanceA);
        assertEq(ch.disputeBalanceB, balanceB);

        vm.warp(block.timestamp + CHALLENGE_WINDOW + 1);

        uint256 aliceBefore = alice.balance;
        uint256 bobBefore = bob.balance;
        vm.prank(bob);
        manager.finalizeClose(channelId);

        assertEq(alice.balance, aliceBefore + balanceA);
        assertEq(bob.balance, bobBefore + balanceB);
        assertEq(uint256(manager.getChannel(channelId).status), uint256(IPaymentChannelManager.ChannelStatus.Closed));
    }

    function test_challengeClose_newestStateWins() public {
        vm.prank(alice);
        uint256 channelId = manager.openChannel{value: 10 ether}(bob);
        vm.prank(bob);
        manager.fundChannel{value: 10 ether}(channelId);

        uint256 balanceA1 = 12 ether;
        uint256 balanceB1 = 8 ether;
        (uint8 vA1, bytes32 rA1, bytes32 sA1) = _signState(channelId, 1, balanceA1, balanceB1, alicePk);
        (uint8 vB1, bytes32 rB1, bytes32 sB1) = _signState(channelId, 1, balanceA1, balanceB1, bobPk);

        vm.prank(alice);
        manager.submitClose(channelId, 1, balanceA1, balanceB1, vA1, rA1, sA1, vB1, rB1, sB1);

        uint256 balanceA2 = 5 ether;
        uint256 balanceB2 = 15 ether;
        (uint8 vA2, bytes32 rA2, bytes32 sA2) = _signState(channelId, 2, balanceA2, balanceB2, alicePk);
        (uint8 vB2, bytes32 rB2, bytes32 sB2) = _signState(channelId, 2, balanceA2, balanceB2, bobPk);

        vm.prank(bob);
        manager.challengeClose(channelId, 2, balanceA2, balanceB2, vA2, rA2, sA2, vB2, rB2, sB2);

        IPaymentChannelManager.Channel memory ch = manager.getChannel(channelId);
        assertEq(ch.disputeNonce, 2);
        assertEq(ch.disputeBalanceA, balanceA2);
        assertEq(ch.disputeBalanceB, balanceB2);

        vm.warp(block.timestamp + CHALLENGE_WINDOW + 1);
        uint256 aliceBefore = alice.balance;
        uint256 bobBefore = bob.balance;
        vm.prank(alice);
        manager.finalizeClose(channelId);

        assertEq(alice.balance, aliceBefore + balanceA2);
        assertEq(bob.balance, bobBefore + balanceB2);
    }

    function test_challengeClose_revertsOlderState() public {
        vm.prank(alice);
        uint256 channelId = manager.openChannel{value: 10 ether}(bob);
        vm.prank(bob);
        manager.fundChannel{value: 10 ether}(channelId);

        uint256 balanceA1 = 12 ether;
        uint256 balanceB1 = 8 ether;
        (uint8 vA1, bytes32 rA1, bytes32 sA1) = _signState(channelId, 1, balanceA1, balanceB1, alicePk);
        (uint8 vB1, bytes32 rB1, bytes32 sB1) = _signState(channelId, 1, balanceA1, balanceB1, bobPk);
        vm.prank(alice);
        manager.submitClose(channelId, 1, balanceA1, balanceB1, vA1, rA1, sA1, vB1, rB1, sB1);

        uint256 balanceA0 = 20 ether;
        uint256 balanceB0 = 0;
        (uint8 vA0, bytes32 rA0, bytes32 sA0) = _signState(channelId, 0, balanceA0, balanceB0, alicePk);
        (uint8 vB0, bytes32 rB0, bytes32 sB0) = _signState(channelId, 0, balanceA0, balanceB0, bobPk);

        vm.prank(bob);
        vm.expectRevert("not newer");
        manager.challengeClose(channelId, 0, balanceA0, balanceB0, vA0, rA0, sA0, vB0, rB0, sB0);
    }

    function test_finalizeClose_revertsBeforeDeadline() public {
        vm.prank(alice);
        uint256 channelId = manager.openChannel{value: 10 ether}(bob);
        vm.prank(bob);
        manager.fundChannel{value: 10 ether}(channelId);

        uint256 balanceA = 10 ether;
        uint256 balanceB = 10 ether;
        (uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, 1, balanceA, balanceB, alicePk);
        (uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, 1, balanceA, balanceB, bobPk);
        vm.prank(alice);
        manager.submitClose(channelId, 1, balanceA, balanceB, vA, rA, sA, vB, rB, sB);

        vm.expectRevert("window open");
        manager.finalizeClose(channelId);
    }

    function test_pauseBlocksOpen() public {
        vm.prank(admin);
        manager.pause();
        vm.prank(alice);
        vm.expectRevert("paused");
        manager.openChannel(bob);
    }

    function test_getChannelCount() public {
        assertEq(manager.getChannelCount(), 0);
        vm.prank(alice);
        manager.openChannel{value: 1 ether}(bob);
        assertEq(manager.getChannelCount(), 1);
    }
}
