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

import "forge-std/Test.sol";
import { StreetCredV3 } from "../../contracts/reputation/StreetCredV3.sol";
import { TaskEscrowV3 } from "../../contracts/escrow/TaskEscrowV3.sol";

/// @notice Directed handler for equally weighted public/private evidence and bounded task bidding.
contract EvidenceBoundsHandler is Test {
    StreetCredV3 public immutable cred;
    TaskEscrowV3 public immutable escrow;
    address public immutable creator;
    bytes32 public constant PUBLIC_SUBJECT = keccak256("stateful-public-evidence");
    bytes32 public constant PRIVATE_SUBJECT = keccak256("stateful-private-evidence");

    uint256 public taskId;
    uint256 public evidencePairs;
    uint256 public boundedLoopRejections;

    constructor(StreetCredV3 _cred, TaskEscrowV3 _escrow) {
        cred = _cred;
        escrow = _escrow;
        creator = address(0xC0FFEE);
    }

    function initializeTask() external {
        if (taskId != 0) return;
        vm.deal(creator, 1 ether);
        vm.prank(creator);
        taskId = escrow.createTaskNative{ value: 1 ether }(
            uint64(block.timestamp + 30 days), uint64(block.timestamp + 31 days), uint64(block.timestamp + 32 days)
        );
    }

    function addMatchedEvidence(uint256 rawWeight) external {
        uint32 weight = uint32(bound(rawWeight, 1, 10_000));
        StreetCredV3.Signal memory publicSignal = StreetCredV3.Signal({
            schemaVersion: 1,
            weightBps: weight,
            evidenceCommitment: keccak256(abi.encode("public", evidencePairs)),
            classification: 2,
            attester: address(this),
            observedAt: uint64(block.timestamp),
            expiresAt: 0,
            negative: false
        });
        StreetCredV3.Signal memory privateSignal = publicSignal;
        privateSignal.evidenceCommitment = keccak256(abi.encode("private commitment", evidencePairs));
        cred.addSignal(PUBLIC_SUBJECT, publicSignal);
        cred.addSignal(PRIVATE_SUBJECT, privateSignal);
        cred.recompute(PUBLIC_SUBJECT, 1);
        cred.recompute(PRIVATE_SUBJECT, 1);
        evidencePairs++;
    }

    function recomputeAtBound() external {
        cred.recompute(PUBLIC_SUBJECT, cred.MAX_RECOMPUTE_LOOKBACK());
        cred.recompute(PRIVATE_SUBJECT, cred.MAX_RECOMPUTE_LOOKBACK());
    }

    function placeNextBid() external {
        if (taskId == 0 || escrow.biddersCount(taskId) == escrow.MAX_BIDS()) return;
        address bidder = address(uint160(0xB10000 + escrow.biddersCount(taskId)));
        vm.prank(bidder);
        escrow.placeBid(taskId);
    }

    function attemptBidOverflow() external {
        if (taskId == 0 || escrow.biddersCount(taskId) != escrow.MAX_BIDS()) return;
        vm.prank(address(0xB1DEEE));
        (bool ok,) = address(escrow).call(abi.encodeCall(TaskEscrowV3.placeBid, (taskId)));
        require(!ok, "unbounded bidder insertion accepted");
        boundedLoopRejections++;
    }
}

/// @notice Stateful invariants for mandatory public/private evidence parity and bounded loops.
contract StatefulEvidenceAndBounds is Test {
    StreetCredV3 internal cred;
    TaskEscrowV3 internal escrow;
    EvidenceBoundsHandler internal handler;

    function setUp() public {
        cred = new StreetCredV3(address(this));
        escrow = new TaskEscrowV3(address(this), address(0xCAFE), 0, 10 ether, 100 ether, 1 days);
        handler = new EvidenceBoundsHandler(cred, escrow);
        cred.setAttester(address(handler), true);
        handler.initializeTask();

        bytes4[] memory selectors = new bytes4[](4);
        selectors[0] = handler.addMatchedEvidence.selector;
        selectors[1] = handler.recomputeAtBound.selector;
        selectors[2] = handler.placeNextBid.selector;
        selectors[3] = handler.attemptBidOverflow.selector;
        excludeContract(address(cred));
        excludeContract(address(escrow));
        targetSelector(FuzzSelector({ addr: address(handler), selectors: selectors }));
    }

    function invariant_privateAndPublicEvidenceRemainEconomicallyEqual() external view {
        (, uint32 publicScore,,,,) = cred.scoreOf(handler.PUBLIC_SUBJECT());
        (, uint32 privateScore,,,,) = cred.scoreOf(handler.PRIVATE_SUBJECT());
        assertEq(privateScore, publicScore, "private evidence received different economic weight");
        assertEq(
            cred.signalCount(handler.PUBLIC_SUBJECT()),
            cred.signalCount(handler.PRIVATE_SUBJECT()),
            "public/private evidence count drift"
        );
    }

    function invariant_userControlledLoopsRemainBounded() external view {
        assertLe(escrow.biddersCount(handler.taskId()), escrow.MAX_BIDS(), "bid registry exceeded hard cap");
        assertLe(cred.MAX_RECOMPUTE_LOOKBACK(), 128, "recompute bound unexpectedly widened");
    }

    function test_handlerRejectsTheThirtyThirdBid() external {
        for (uint256 i; i < escrow.MAX_BIDS(); ++i) {
            handler.placeNextBid();
        }
        handler.attemptBidOverflow();
        assertEq(escrow.biddersCount(handler.taskId()), escrow.MAX_BIDS());
        assertEq(handler.boundedLoopRejections(), 1);
    }
}
