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

import { Test } from "forge-std/Test.sol";
import { ThresholdConsensusV3 } from "../../contracts/consensus/ThresholdConsensusV3.sol";

contract ThresholdConsensusV3Test is Test {
    address internal constant OWNER = address(0xA11CE);
    address internal constant ATTESTER_A = address(0xA);
    address internal constant ATTESTER_B = address(0xB);
    bytes32 internal constant SCHEMA = keccak256("schema");
    bytes32 internal constant SUBJECT = keccak256("subject");
    bytes32 internal constant SNAPSHOT = keccak256("snapshot");
    bytes32 internal constant OUTCOME = keccak256("outcome");

    ThresholdConsensusV3 internal consensus;

    function setUp() external {
        consensus = new ThresholdConsensusV3(OWNER, 2);
        vm.startPrank(OWNER);
        consensus.setAttester(ATTESTER_A, true);
        consensus.setAttester(ATTESTER_B, true);
        vm.stopPrank();
    }

    function test_FinalizedClaimCannotBeUsedAfterExpiry() external {
        uint64 expiresAt = uint64(block.timestamp + 1 days);
        ThresholdConsensusV3.Claim memory claim = ThresholdConsensusV3.Claim({
            schema: SCHEMA,
            subject: SUBJECT,
            nonce: 1,
            expiresAt: expiresAt,
            settlementSnapshot: SNAPSHOT,
            outcome: OUTCOME
        });

        vm.prank(ATTESTER_A);
        consensus.attest(claim);
        vm.prank(ATTESTER_B);
        consensus.attest(claim);

        assertTrue(consensus.isFinalized(SCHEMA, SUBJECT, 1, SNAPSHOT, OUTCOME));
        vm.warp(expiresAt);
        assertFalse(consensus.isFinalized(SCHEMA, SUBJECT, 1, SNAPSHOT, OUTCOME));
    }

    /// @notice A removed attester's partial approval must not survive a rotation.
    /// @dev Before the fix, A could vote, governance could remove A/add C, and C's
    ///      first vote would finalize the claim at threshold two using A's revoked vote.
    function test_attesterRotationInvalidatesPartialClaimAndRequiresFreshThreshold() external {
        address attesterC = address(0xC);
        uint64 expiresAt = uint64(block.timestamp + 1 days);
        ThresholdConsensusV3.Claim memory claim = ThresholdConsensusV3.Claim({
            schema: SCHEMA,
            subject: SUBJECT,
            nonce: 2,
            expiresAt: expiresAt,
            settlementSnapshot: SNAPSHOT,
            outcome: OUTCOME
        });

        vm.prank(ATTESTER_A);
        consensus.attest(claim);
        bytes32 key = consensus.claimKey(SCHEMA, SUBJECT, 2);
        assertTrue(consensus.voted(key, ATTESTER_A));

        vm.startPrank(OWNER);
        consensus.setAttester(ATTESTER_A, false);
        consensus.setAttester(attesterC, true);
        vm.stopPrank();

        assertTrue(consensus.voted(key, ATTESTER_A), "historical vote was lost");
        vm.prank(attesterC);
        consensus.attest(claim);
        assertFalse(consensus.isFinalized(SCHEMA, SUBJECT, 2, SNAPSHOT, OUTCOME), "stale approval finalized");

        vm.prank(ATTESTER_B);
        consensus.attest(claim);
        assertTrue(consensus.isFinalized(SCHEMA, SUBJECT, 2, SNAPSHOT, OUTCOME));
    }

    function test_thresholdChangeInvalidatesPartialClaim() external {
        address attesterC = address(0xC);
        vm.startPrank(OWNER);
        consensus.setAttester(attesterC, true);
        vm.stopPrank();

        uint64 expiresAt = uint64(block.timestamp + 1 days);
        ThresholdConsensusV3.Claim memory claim = ThresholdConsensusV3.Claim({
            schema: SCHEMA,
            subject: SUBJECT,
            nonce: 3,
            expiresAt: expiresAt,
            settlementSnapshot: SNAPSHOT,
            outcome: OUTCOME
        });

        vm.prank(ATTESTER_A);
        consensus.attest(claim);
        vm.prank(OWNER);
        consensus.setThreshold(3);
        vm.prank(ATTESTER_B);
        consensus.attest(claim);
        vm.prank(attesterC);
        consensus.attest(claim);
        assertFalse(
            consensus.isFinalized(SCHEMA, SUBJECT, 3, SNAPSHOT, OUTCOME), "old vote counted after threshold change"
        );

        vm.prank(ATTESTER_A);
        consensus.attest(claim);
        assertTrue(consensus.isFinalized(SCHEMA, SUBJECT, 3, SNAPSHOT, OUTCOME));
    }

    /// @dev The public getter is consumed by bond self-attestation guards, so
    ///      a later attester rotation must not hide a vote in an already-final claim.
    function test_finalizedClaimRetainsHistoricalVoteForSelfAttestationGuard() external {
        uint64 expiresAt = uint64(block.timestamp + 1 days);
        ThresholdConsensusV3.Claim memory claim = ThresholdConsensusV3.Claim({
            schema: SCHEMA,
            subject: SUBJECT,
            nonce: 4,
            expiresAt: expiresAt,
            settlementSnapshot: SNAPSHOT,
            outcome: OUTCOME
        });
        bytes32 key = consensus.claimKey(SCHEMA, SUBJECT, 4);

        vm.prank(ATTESTER_A);
        consensus.attest(claim);
        vm.prank(ATTESTER_B);
        consensus.attest(claim);
        assertTrue(consensus.isFinalized(SCHEMA, SUBJECT, 4, SNAPSHOT, OUTCOME));

        vm.prank(OWNER);
        consensus.setAttester(address(0xC), true);
        assertTrue(consensus.voted(key, ATTESTER_A));
        assertTrue(consensus.voted(key, ATTESTER_B));
    }
}
