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

import { Test } from "forge-std/Test.sol";
import { DisputeRegistryV3 } from "../../contracts/disputes/DisputeRegistryV3.sol";
import { IDisputeSubjectV3 } from "../../contracts/disputes/IDisputeSubjectV3.sol";

contract DisputeSubjectMock is IDisputeSubjectV3 {
    address internal immutable challengerA;
    address internal immutable challengerB;
    uint64 internal immutable settlementRequestedAt;
    uint64 internal immutable challengeDeadline;

    constructor(address challengerA_, address challengerB_) {
        challengerA = challengerA_;
        challengerB = challengerB_;
        settlementRequestedAt = uint64(block.timestamp);
        challengeDeadline = uint64(block.timestamp + 1 days);
    }

    function disputeSubject(uint256 subjectId) external view returns (bytes32) {
        return keccak256(abi.encode(address(this), subjectId));
    }

    function isAuthorizedChallenger(uint256, address account) external view returns (bool) {
        return account == challengerA || account == challengerB;
    }

    function authorizedRespondent(uint256, address account) external view returns (address) {
        return account == challengerA ? address(0xBEEF) : account == challengerB ? challengerA : address(0);
    }

    function disputePayoutSource(uint256) external view returns (address) {
        return address(this);
    }

    function disputePayoutCap(uint256) external pure returns (uint256) {
        return 0;
    }

    function settlementChallengeContext(uint256 subjectId) external view returns (bytes32, uint64, uint64) {
        return (keccak256(abi.encode("settlement", subjectId)), settlementRequestedAt, challengeDeadline);
    }
}

contract DisputeRegistryV3Test is Test {
    address internal constant OWNER = address(0xA11CE);
    address internal constant CHALLENGER = address(0xC0FFEE);
    address internal constant OPPOSITE_CHALLENGER = address(0xC0FFEF);
    address internal constant ADJUDICATOR_A = address(0xA);
    address internal constant ADJUDICATOR_B = address(0xB);
    address internal constant ADJUDICATOR_C = address(0xC);
    address internal constant ADJUDICATOR_D = address(0xD);
    address internal constant ADJUDICATOR_E = address(0xE);

    DisputeRegistryV3 internal registry;
    DisputeSubjectMock internal subject;

    function setUp() external {
        registry = new DisputeRegistryV3(OWNER);
        subject = new DisputeSubjectMock(CHALLENGER, OPPOSITE_CHALLENGER);
        vm.startPrank(OWNER);
        registry.setAdjudicator(ADJUDICATOR_A, true);
        registry.setAdjudicator(ADJUDICATOR_B, true);
        registry.setAdjudicator(ADJUDICATOR_C, true);
        registry.setAdjudicator(ADJUDICATOR_D, true);
        registry.setAdjudicator(ADJUDICATOR_E, true);
        registry.setAdjudicatorThreshold(3);
        vm.stopPrank();
    }

    function testFuzz_MembershipRotationPreservesFiledEligibilitySnapshot(uint64 responseDelay, uint64 decisionDelay)
        external
    {
        // The mock's settlement challenge deadline is one day after filing.
        // Keep fuzzed deadlines inside the protocol's actionable window.
        responseDelay = uint64(bound(responseDelay, 1 hours, 12 hours));
        decisionDelay = uint64(bound(decisionDelay, responseDelay + 1, 23 hours));

        vm.prank(CHALLENGER);
        uint256 disputeId = registry.fileDispute(
            address(subject),
            1,
            keccak256("evidence"),
            uint64(block.timestamp) + responseDelay,
            uint64(block.timestamp) + decisionDelay,
            address(subject),
            0
        );

        vm.prank(address(0xBEEF));
        registry.respond(disputeId);
        vm.prank(ADJUDICATOR_A);
        registry.vote(disputeId, true);

        vm.prank(OWNER);
        registry.setAdjudicator(ADJUDICATOR_A, false);

        // Removal after filing cannot erase A's already-cast vote or prevent
        // the other filing-time adjudicators from completing the dispute.
        vm.prank(ADJUDICATOR_B);
        registry.vote(disputeId, true);
        vm.prank(ADJUDICATOR_C);
        registry.vote(disputeId, true);

        DisputeRegistryV3.Dispute memory dispute = registry.disputeOf(disputeId);
        assertTrue(dispute.finalized);
        assertEq(dispute.approvals, 3);
    }

    function test_rotationCannotAddEligibleVoterToOpenDispute() external {
        uint256 disputeId = _fileDispute();

        vm.startPrank(OWNER);
        registry.setAdjudicator(ADJUDICATOR_E, false);
        address replacement = address(0xF);
        registry.setAdjudicator(replacement, true);
        vm.stopPrank();

        assertFalse(registry.eligibleAdjudicator(disputeId, replacement));
        vm.prank(replacement);
        vm.expectRevert(DisputeRegistryV3.NotAdjudicator.selector);
        registry.vote(disputeId, true);
    }

    function test_removedFiledAdjudicatorCanStillVoteOpenDispute() external {
        uint256 disputeId = _fileDispute();
        _respond(disputeId);

        vm.prank(OWNER);
        registry.setAdjudicator(ADJUDICATOR_A, false);

        assertFalse(registry.adjudicator(ADJUDICATOR_A));
        assertTrue(registry.eligibleAdjudicator(disputeId, ADJUDICATOR_A));
        vm.prank(ADJUDICATOR_A);
        registry.vote(disputeId, true);
        assertEq(registry.disputeOf(disputeId).approvals, 1);
    }

    function test_refundableFilingBondReturnedExactlyOnceWhenUpheld() external {
        vm.deal(CHALLENGER, 1 ether);
        vm.prank(CHALLENGER);
        uint256 disputeId = registry.fileDispute{ value: 1 ether }(
            address(subject),
            1,
            keccak256("refundable-bond"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            address(subject),
            0
        );

        assertEq(registry.disputeOf(disputeId).refundableBondWei, 1 ether);
        _respond(disputeId);
        vm.prank(ADJUDICATOR_A);
        registry.vote(disputeId, true);
        vm.prank(ADJUDICATOR_B);
        registry.vote(disputeId, true);
        vm.prank(ADJUDICATOR_C);
        registry.vote(disputeId, true);
        assertEq(registry.creditOf(address(0), CHALLENGER), 1 ether);

        vm.prank(ADJUDICATOR_D);
        vm.expectRevert(DisputeRegistryV3.AlreadyFinalized.selector);
        registry.vote(disputeId, true);
        assertEq(registry.creditOf(address(0), CHALLENGER), 1 ether);
    }

    function test_votesCannotPrematurelyFinalizeBeforeResponsePeriod() external {
        uint256 disputeId = _fileDispute();

        vm.prank(ADJUDICATOR_A);
        vm.expectRevert(DisputeRegistryV3.ResponsePending.selector);
        registry.vote(disputeId, true);
        assertEq(registry.disputeOf(disputeId).approvals, 0);

        _respond(disputeId);
        vm.prank(ADJUDICATOR_A);
        registry.vote(disputeId, true);
        assertEq(registry.disputeOf(disputeId).approvals, 1);
        assertFalse(registry.disputeOf(disputeId).finalized);
    }

    function test_cannotFileDuplicateDisputeForSettlementAttempt() external {
        _fileDispute();

        vm.prank(CHALLENGER);
        vm.expectRevert(DisputeRegistryV3.DuplicateDispute.selector);
        registry.fileDispute(
            address(subject),
            1,
            keccak256("different-evidence-same-attempt"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            address(subject),
            0
        );
    }

    function test_oppositeAuthorizedChallengerCanFileForSameSettlementAttempt() external {
        uint256 firstDisputeId = _fileDispute();

        vm.prank(OPPOSITE_CHALLENGER);
        uint256 secondDisputeId = registry.fileDispute(
            address(subject),
            1,
            keccak256("opposite-authorized-challenger"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            address(subject),
            0
        );

        assertEq(registry.disputeCount(), 2);
        assertEq(registry.disputeOf(firstDisputeId).challenger, CHALLENGER);
        assertEq(registry.disputeOf(secondDisputeId).challenger, OPPOSITE_CHALLENGER);
        assertEq(
            registry.disputeOf(firstDisputeId).settlementSnapshot,
            registry.disputeOf(secondDisputeId).settlementSnapshot
        );
        assertEq(
            registry.disputeOf(firstDisputeId).settlementRequestedAt,
            registry.disputeOf(secondDisputeId).settlementRequestedAt
        );
    }

    function test_timeoutForOnePartyDoesNotBlockOppositePartysMeaningfulChallengeOrReplayFinalization() external {
        vm.deal(CHALLENGER, 1 ether);
        vm.prank(CHALLENGER);
        uint256 timeoutDisputeId = registry.fileDispute{ value: 1 ether }(
            address(subject),
            1,
            keccak256("short-lived-timeout"),
            uint64(block.timestamp + 30 minutes),
            uint64(block.timestamp + 1 hours),
            address(subject),
            0
        );

        vm.warp(block.timestamp + 1 hours);
        registry.finalizeByTimeout(timeoutDisputeId);
        assertEq(registry.creditOf(address(0), CHALLENGER), 1 ether);
        assertEq(uint256(registry.disputeOf(timeoutDisputeId).outcome), uint256(DisputeRegistryV3.Outcome.Rejected));
        vm.expectRevert(DisputeRegistryV3.AlreadyFinalized.selector);
        registry.finalizeByTimeout(timeoutDisputeId);

        vm.deal(OPPOSITE_CHALLENGER, 1 ether);
        vm.prank(OPPOSITE_CHALLENGER);
        uint256 meaningfulDisputeId = registry.fileDispute{ value: 1 ether }(
            address(subject),
            1,
            keccak256("opposite-party-meaningful-challenge"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            address(subject),
            0
        );

        vm.prank(CHALLENGER);
        registry.respond(meaningfulDisputeId);
        vm.prank(ADJUDICATOR_A);
        registry.vote(meaningfulDisputeId, true);
        vm.prank(ADJUDICATOR_B);
        registry.vote(meaningfulDisputeId, true);
        vm.prank(ADJUDICATOR_C);
        registry.vote(meaningfulDisputeId, true);

        assertEq(registry.creditOf(address(0), OPPOSITE_CHALLENGER), 1 ether);
        assertTrue(registry.disputeOf(meaningfulDisputeId).finalized);
        assertEq(uint256(registry.disputeOf(meaningfulDisputeId).outcome), uint256(DisputeRegistryV3.Outcome.Upheld));

        vm.expectRevert(DisputeRegistryV3.AlreadyFinalized.selector);
        registry.finalizeByTimeout(meaningfulDisputeId);
        vm.prank(ADJUDICATOR_D);
        vm.expectRevert(DisputeRegistryV3.AlreadyFinalized.selector);
        registry.vote(meaningfulDisputeId, true);
    }

    function test_refundableFilingBondReturnedExactlyOnceWhenRejected() external {
        vm.deal(CHALLENGER, 1 ether);
        vm.prank(CHALLENGER);
        uint256 disputeId = registry.fileDispute{ value: 1 ether }(
            address(subject),
            1,
            keccak256("refundable-rejected-bond"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            address(subject),
            0
        );

        vm.warp(block.timestamp + 2 hours);
        registry.finalizeByTimeout(disputeId);
        assertEq(registry.creditOf(address(0), CHALLENGER), 1 ether);
        assertEq(uint256(registry.disputeOf(disputeId).outcome), uint256(DisputeRegistryV3.Outcome.Rejected));

        vm.expectRevert(DisputeRegistryV3.AlreadyFinalized.selector);
        registry.finalizeByTimeout(disputeId);
        assertEq(registry.creditOf(address(0), CHALLENGER), 1 ether);
    }

    function test_rejectsDecisionDeadlineAtOrAfterSettlementChallengeDeadline() external {
        vm.prank(CHALLENGER);
        vm.expectRevert(DisputeRegistryV3.BadDeadlines.selector);
        registry.fileDispute(
            address(subject),
            1,
            keccak256("deadline-at-cutoff"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 1 days),
            address(subject),
            0
        );

        vm.prank(CHALLENGER);
        vm.expectRevert(DisputeRegistryV3.BadDeadlines.selector);
        registry.fileDispute(
            address(subject),
            1,
            keccak256("deadline-after-cutoff"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 days),
            address(subject),
            0
        );
    }

    function test_acceptsDecisionDeadlineStrictlyBeforeSettlementChallengeDeadline() external {
        vm.prank(CHALLENGER);
        uint256 disputeId = registry.fileDispute(
            address(subject),
            1,
            keccak256("deadline-before-cutoff"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 1 days - 1),
            address(subject),
            0
        );
        DisputeRegistryV3.Dispute memory dispute = registry.disputeOf(disputeId);
        assertLt(dispute.decisionDeadline, dispute.challengeDeadline);
    }

    function test_legacyThresholdOneAttemptCannotLowerGovernanceConfiguredThreeOfFive() external {
        vm.prank(CHALLENGER);
        uint256 disputeId = registry.fileDispute(
            address(subject),
            1,
            keccak256("threshold-one-attempt"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            1,
            address(subject),
            0
        );

        DisputeRegistryV3.Dispute memory dispute = registry.disputeOf(disputeId);
        assertEq(dispute.adjudicatorThreshold, 3, "challenger lowered the configured threshold");

        _respond(disputeId);
        vm.prank(ADJUDICATOR_A);
        registry.vote(disputeId, true);
        dispute = registry.disputeOf(disputeId);
        assertEq(dispute.approvals, 1);
        assertFalse(dispute.finalized, "one of five adjudicators upheld the dispute");
    }

    function test_filingFailsClosedUntilGovernanceConfiguresThreshold() external {
        DisputeRegistryV3 unconfiguredRegistry = new DisputeRegistryV3(OWNER);
        vm.prank(OWNER);
        unconfiguredRegistry.setAdjudicator(ADJUDICATOR_A, true);

        vm.prank(CHALLENGER);
        vm.expectRevert(DisputeRegistryV3.BadThreshold.selector);
        unconfiguredRegistry.fileDispute(
            address(subject),
            1,
            keccak256("unconfigured-threshold"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            address(subject),
            0
        );
    }

    function test_thresholdChangeAfterFilingDoesNotChangeSnapshot() external {
        uint256 disputeId = _fileDispute();

        vm.prank(OWNER);
        registry.setAdjudicatorThreshold(1);

        assertEq(registry.requiredAdjudicatorThreshold(), 1);
        assertEq(registry.disputeOf(disputeId).adjudicatorThreshold, 3, "open dispute threshold was not snapshotted");

        _respond(disputeId);
        vm.prank(ADJUDICATOR_A);
        registry.vote(disputeId, true);
        assertFalse(registry.disputeOf(disputeId).finalized, "post-filing configuration lowered threshold");
    }

    function test_replayedVoteAndInsufficientApprovalsCannotFinalizeThreeOfFiveDispute() external {
        uint256 disputeId = _fileDispute();
        _respond(disputeId);

        vm.prank(ADJUDICATOR_A);
        registry.vote(disputeId, true);
        vm.prank(ADJUDICATOR_A);
        vm.expectRevert(DisputeRegistryV3.AlreadyVoted.selector);
        registry.vote(disputeId, true);
        vm.prank(ADJUDICATOR_B);
        registry.vote(disputeId, true);

        DisputeRegistryV3.Dispute memory dispute = registry.disputeOf(disputeId);
        assertEq(dispute.approvals, 2);
        assertFalse(dispute.finalized, "insufficient approvals upheld the dispute");
    }

    function test_validGovernanceThresholdApprovalsFinalizeDispute() external {
        uint256 disputeId = _fileDispute();
        _respond(disputeId);

        vm.prank(ADJUDICATOR_A);
        registry.vote(disputeId, true);
        vm.prank(ADJUDICATOR_B);
        registry.vote(disputeId, true);
        vm.prank(ADJUDICATOR_C);
        registry.vote(disputeId, true);

        DisputeRegistryV3.Dispute memory dispute = registry.disputeOf(disputeId);
        assertEq(dispute.approvals, 3);
        assertTrue(dispute.finalized);
        assertEq(uint256(dispute.outcome), uint256(DisputeRegistryV3.Outcome.Upheld));
    }

    function testFuzz_legacyThresholdInputCannotAffectSnapshot(uint8 attemptedThreshold) external {
        vm.prank(CHALLENGER);
        uint256 disputeId = registry.fileDispute(
            address(subject),
            1,
            keccak256(abi.encode("fuzzed-threshold-input", attemptedThreshold)),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            attemptedThreshold,
            address(subject),
            0
        );

        assertEq(registry.disputeOf(disputeId).adjudicatorThreshold, 3);
    }

    function _fileDispute() internal returns (uint256) {
        vm.prank(CHALLENGER);
        return registry.fileDispute(
            address(subject),
            1,
            keccak256("governance-snapshotted-threshold"),
            uint64(block.timestamp + 1 hours),
            uint64(block.timestamp + 2 hours),
            address(subject),
            0
        );
    }

    function _respond(uint256 disputeId) internal {
        vm.prank(address(0xBEEF));
        registry.respond(disputeId);
    }
}
