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

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

contract StreetCredV3Test is Test {
    address internal constant OWNER = address(0xA11CE);
    address internal constant ATTESTER = address(0xA);
    address internal constant SPOOFED_ATTESTER = address(0xB);

    StreetCredV3 internal streetCred;

    function setUp() external {
        streetCred = new StreetCredV3(OWNER);
        vm.prank(OWNER);
        streetCred.setAttester(ATTESTER, true);
    }

    function test_RejectsSpoofedSignalAttester() external {
        StreetCredV3.Signal memory signal = StreetCredV3.Signal({
            schemaVersion: 1,
            weightBps: 10_000,
            evidenceCommitment: keccak256("evidence"),
            classification: 2,
            attester: SPOOFED_ATTESTER,
            observedAt: uint64(block.timestamp),
            expiresAt: 0,
            negative: false
        });

        vm.prank(ATTESTER);
        vm.expectRevert(StreetCredV3.AttesterMismatch.selector);
        streetCred.addSignal(keccak256("subject"), signal);
    }

    function testFuzz_RecordsAuthenticatedSignal(uint32 weight) external {
        weight = uint32(bound(weight, 0, 10_000));
        StreetCredV3.Signal memory signal = StreetCredV3.Signal({
            schemaVersion: 1,
            weightBps: weight,
            evidenceCommitment: keccak256("evidence"),
            classification: 2,
            attester: ATTESTER,
            observedAt: uint64(block.timestamp),
            expiresAt: 0,
            negative: false
        });

        bytes32 subject = keccak256("subject");
        vm.prank(ATTESTER);
        streetCred.addSignal(subject, signal);
        assertEq(streetCred.signalAt(subject, 0).attester, ATTESTER);
    }

    function test_rejectsZeroSubjectEvidenceFutureObservationAndUnknownClass() external {
        StreetCredV3.Signal memory signal = _signal(100, false);
        vm.startPrank(ATTESTER);
        vm.expectRevert(StreetCredV3.InvalidSignal.selector);
        streetCred.addSignal(bytes32(0), signal);
        signal.evidenceCommitment = bytes32(0);
        vm.expectRevert(StreetCredV3.InvalidSignal.selector);
        streetCred.addSignal(keccak256("subject"), signal);
        signal = _signal(100, false);
        signal.observedAt = uint64(block.timestamp + 1);
        vm.expectRevert(StreetCredV3.InvalidSignal.selector);
        streetCred.addSignal(keccak256("subject"), signal);
        signal = _signal(100, false);
        signal.classification = 7;
        vm.expectRevert(StreetCredV3.InvalidSignal.selector);
        streetCred.addSignal(keccak256("subject"), signal);
        vm.stopPrank();
    }

    function test_scoreIsOrderIndependentAndEvidenceRootIsPopulated() external {
        bytes32 first = keccak256("first");
        bytes32 second = keccak256("second");
        vm.startPrank(ATTESTER);
        streetCred.addSignal(first, _signal(8_000, false));
        streetCred.addSignal(first, _signal(3_000, true));
        streetCred.addSignal(second, _signal(3_000, true));
        streetCred.addSignal(second, _signal(8_000, false));
        streetCred.recompute(first, 2);
        streetCred.recompute(second, 2);
        vm.stopPrank();
        (uint32 schemaA, uint32 scoreA, uint32 countA, bytes32 rootA,,) = streetCred.scoreOf(first);
        (, uint32 scoreB, uint32 countB, bytes32 rootB,,) = streetCred.scoreOf(second);
        assertEq(schemaA, 1);
        assertEq(scoreA, scoreB);
        assertEq(countA, 2);
        assertEq(countB, 2);
        assertTrue(rootA != bytes32(0));
        assertTrue(rootB != bytes32(0));
    }

    function test_signalExpiringExactlyNowIsExcluded() external {
        StreetCredV3.Signal memory signal = _signal(10_000, false);
        signal.expiresAt = uint64(block.timestamp + 1);
        bytes32 subject = keccak256("expiry");
        vm.prank(ATTESTER);
        streetCred.addSignal(subject, signal);
        vm.warp(block.timestamp + 1);
        vm.prank(ATTESTER);
        streetCred.recompute(subject, 1);
        (, uint32 score, uint32 count, bytes32 root,,) = streetCred.scoreOf(subject);
        assertEq(score, 0);
        assertEq(count, 0);
        assertEq(root, bytes32(0));
    }

    function _signal(uint32 weight, bool negative) private view returns (StreetCredV3.Signal memory) {
        return StreetCredV3.Signal({
            schemaVersion: 1,
            weightBps: weight,
            evidenceCommitment: keccak256(abi.encode(weight, negative)),
            classification: 2,
            attester: ATTESTER,
            observedAt: uint64(block.timestamp),
            expiresAt: 0,
            negative: negative
        });
    }
}
