// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.24;

import {EveryChannelObservationLedger} from "../EveryChannelObservationLedger.sol";
import {EveryChannelWitnessRegistry} from "../EveryChannelWitnessRegistry.sol";

contract WitnessActor {
    function propose(
        EveryChannelObservationLedger ledger,
        EveryChannelObservationLedger.ObservationHeader calldata header
    ) external returns (bytes32) {
        return ledger.proposeObservation(header);
    }

    function attest(
        EveryChannelObservationLedger ledger,
        bytes32 observationHash
    ) external {
        ledger.attestObservation(observationHash);
    }
}

contract ObservationLedgerTest {
    function _header(
        bytes32 streamHash,
        bytes32 epochHash,
        uint64 sequence,
        bytes32 dataRoot
    ) internal pure returns (EveryChannelObservationLedger.ObservationHeader memory) {
        return EveryChannelObservationLedger.ObservationHeader({
            streamHash: streamHash,
            epochHash: epochHash,
            parentObservationHash: bytes32(0),
            dataRoot: dataRoot,
            locatorHash: keccak256(abi.encodePacked(streamHash, epochHash, sequence)),
            observedUnixMs: 1_772_001_256_329,
            sequence: sequence
        });
    }

    function test_finalizes_when_quorum_is_reached() public {
        EveryChannelWitnessRegistry registry = new EveryChannelWitnessRegistry(address(this));
        WitnessActor witnessA = new WitnessActor();
        WitnessActor witnessB = new WitnessActor();
        registry.addWitness(address(witnessA));
        registry.addWitness(address(witnessB));

        EveryChannelObservationLedger ledger = new EveryChannelObservationLedger(
            address(registry),
            2
        );

        EveryChannelObservationLedger.ObservationHeader memory header = _header(
            keccak256("la-cbs:video0.m4s"),
            keccak256("epoch-229"),
            229,
            keccak256("58cd13f693debd000d995c9a5574e8b9274cc4d3399eb6f1f22393af1ba7407d")
        );

        bytes32 observationHash = witnessA.propose(ledger, header);
        bytes32 slot = ledger.observationSlot(header.streamHash, header.epochHash);

        assert(ledger.finalizedObservationBySlot(slot) == bytes32(0));

        witnessB.attest(ledger, observationHash);

        assert(ledger.finalizedObservationBySlot(slot) == observationHash);
        EveryChannelObservationLedger.ObservationHeader memory storedHeader = ledger
            .getObservationHeader(observationHash);
        assert(storedHeader.streamHash == header.streamHash);
        assert(storedHeader.epochHash == header.epochHash);
        assert(storedHeader.dataRoot == header.dataRoot);
        assert(storedHeader.sequence == header.sequence);
    }

    function test_rejects_duplicate_attestation() public {
        EveryChannelWitnessRegistry registry = new EveryChannelWitnessRegistry(address(this));
        WitnessActor witnessA = new WitnessActor();
        registry.addWitness(address(witnessA));

        EveryChannelObservationLedger ledger = new EveryChannelObservationLedger(
            address(registry),
            1
        );

        EveryChannelObservationLedger.ObservationHeader memory header = _header(
            keccak256("la-nbc:catalog.json"),
            keccak256("epoch-5"),
            5,
            keccak256("f6ea0793fd00e29ced670d586e0e5f7f3d0f5edfc016c03f80710bd4bed587ec")
        );

        bytes32 observationHash = witnessA.propose(ledger, header);
        assert(
            ledger.finalizedObservationBySlot(
                ledger.observationSlot(header.streamHash, header.epochHash)
            ) == observationHash
        );

        (bool ok, ) = address(witnessA).call(
            abi.encodeWithSelector(
                WitnessActor.attest.selector,
                ledger,
                observationHash
            )
        );
        assert(!ok);
    }

    function test_competing_candidates_only_one_slot_can_finalize() public {
        EveryChannelWitnessRegistry registry = new EveryChannelWitnessRegistry(address(this));
        WitnessActor witnessA = new WitnessActor();
        WitnessActor witnessB = new WitnessActor();
        WitnessActor witnessC = new WitnessActor();
        registry.addWitness(address(witnessA));
        registry.addWitness(address(witnessB));
        registry.addWitness(address(witnessC));

        EveryChannelObservationLedger ledger = new EveryChannelObservationLedger(
            address(registry),
            2
        );

        bytes32 streamHash = keccak256("la-cbs:video0.m4s");
        bytes32 epochHash = keccak256("epoch-230");
        EveryChannelObservationLedger.ObservationHeader memory candidateA = _header(
            streamHash,
            epochHash,
            230,
            keccak256("1130c51b3ce428505dbc3c3294678f76e3200221d853fc9b02c8ed603ecc8b8c")
        );
        EveryChannelObservationLedger.ObservationHeader memory candidateB = _header(
            streamHash,
            epochHash,
            230,
            keccak256("deadbeef")
        );

        bytes32 hashA = witnessA.propose(ledger, candidateA);
        bytes32 hashB = witnessB.propose(ledger, candidateB);
        bytes32 slot = ledger.observationSlot(streamHash, epochHash);

        assert(hashA != hashB);
        assert(ledger.finalizedObservationBySlot(slot) == bytes32(0));

        witnessC.attest(ledger, hashA);
        assert(ledger.finalizedObservationBySlot(slot) == hashA);

        (bool ok, ) = address(witnessB).call(
            abi.encodeWithSelector(
                WitnessActor.attest.selector,
                ledger,
                hashB
            )
        );
        assert(!ok);
    }
}
