//! Proposals from validators

mod canonical_proposal;
mod msg_type;
mod sign_proposal;

pub use self::canonical_proposal::CanonicalProposal;
pub use msg_type::Type;
pub use sign_proposal::{SignProposalRequest, SignedProposalResponse};

use crate::block::{Height, Id as BlockId, Round};
use crate::chain::Id as ChainId;
use crate::consensus::State;
use crate::Signature;
use crate::Time;
use crate::{Error, Kind};
use bytes::BufMut;
use std::convert::{TryFrom, TryInto};
use tendermint_proto::types::Proposal as RawProposal;
use tendermint_proto::{Error as ProtobufError, Protobuf};

/// Proposal
#[derive(Clone, PartialEq, Debug)]
pub struct Proposal {
    /// Proposal message type
    pub msg_type: Type,
    /// Height
    pub height: Height,
    /// Round
    pub round: Round,
    /// POL Round
    pub pol_round: Option<Round>,
    /// Block ID
    pub block_id: Option<BlockId>,
    /// Timestamp
    pub timestamp: Option<Time>,
    /// Signature
    pub signature: Signature,
}

impl Protobuf<RawProposal> for Proposal {}

impl TryFrom<RawProposal> for Proposal {
    type Error = Error;

    fn try_from(value: RawProposal) -> Result<Self, Self::Error> {
        if value.pol_round < -1 {
            return Err(Kind::NegativePolRound.into());
        }
        let pol_round = match value.pol_round {
            -1 => None,
            n => Some(Round::try_from(n)?),
        };
        Ok(Proposal {
            msg_type: value.r#type.try_into()?,
            height: value.height.try_into()?,
            round: value.round.try_into()?,
            pol_round,
            block_id: value.block_id.map(TryInto::try_into).transpose()?,
            timestamp: value.timestamp.map(TryInto::try_into).transpose()?,
            signature: value.signature.try_into()?,
        })
    }
}

impl From<Proposal> for RawProposal {
    fn from(value: Proposal) -> Self {
        RawProposal {
            r#type: value.msg_type.into(),
            height: value.height.into(),
            round: value.round.into(),
            pol_round: value.pol_round.map_or(-1, Into::into),
            block_id: value.block_id.map(Into::into),
            timestamp: value.timestamp.map(Into::into),
            signature: value.signature.into(),
        }
    }
}

impl Proposal {
    /// Create signable bytes from Proposal.
    pub fn to_signable_bytes<B>(
        &self,
        chain_id: ChainId,
        sign_bytes: &mut B,
    ) -> Result<bool, ProtobufError>
    where
        B: BufMut,
    {
        CanonicalProposal::new(self.clone(), chain_id).encode_length_delimited(sign_bytes)?;
        Ok(true)
    }

    /// Create signable vector from Proposal.
    pub fn to_signable_vec(&self, chain_id: ChainId) -> Result<Vec<u8>, ProtobufError> {
        CanonicalProposal::new(self.clone(), chain_id).encode_length_delimited_vec()
    }

    /// Consensus state from this proposal - This doesn't seem to be used anywhere.
    #[deprecated(
        since = "0.17.0",
        note = "This seems unnecessary, please raise it to the team, if you need it."
    )]
    pub fn consensus_state(&self) -> State {
        State {
            height: self.height,
            round: self.round,
            step: 3,
            block_id: self.block_id,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::block::parts::Header;
    use crate::block::Id as BlockId;
    use crate::block::{Height, Round};
    use crate::chain::Id as ChainId;
    use crate::hash::{Algorithm, Hash};
    use crate::proposal::SignProposalRequest;
    use crate::signature::{Ed25519Signature, ED25519_SIGNATURE_SIZE};
    use crate::{proposal::Type, Proposal, Signature};
    use chrono::{DateTime, Utc};
    use std::str::FromStr;
    use tendermint_proto::Protobuf;

    #[test]
    fn test_serialization() {
        let dt = "2018-02-11T07:09:22.765Z".parse::<DateTime<Utc>>().unwrap();
        let proposal = Proposal {
            msg_type: Type::Proposal,
            height: Height::from(12345_u32),
            round: Round::from(23456_u16),
            pol_round: None,
            block_id: Some(BlockId {
                hash: Hash::from_hex_upper(
                    Algorithm::Sha256,
                    "DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA",
                )
                .unwrap(),
                part_set_header: Header::new(
                    65535,
                    Hash::from_hex_upper(
                        Algorithm::Sha256,
                        "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
                    )
                    .unwrap(),
                )
                .unwrap(),
            }),
            timestamp: Some(dt.into()),
            signature: Signature::Ed25519(Ed25519Signature::new([0; ED25519_SIGNATURE_SIZE])),
        };

        let mut got = vec![];

        let request = SignProposalRequest {
            proposal,
            chain_id: ChainId::from_str("test_chain_id").unwrap(),
        };

        let _have = request.to_signable_bytes(&mut got);

        // the following vector is generated via:
        /*
            import (
                "encoding/hex"
                "fmt"
                prototypes "github.com/tendermint/tendermint/proto/tendermint/types"
                "github.com/tendermint/tendermint/types"
                "strings"
                "time"
            )

            func proposalSerialize() {
                stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z")
                block_hash, _ := hex.DecodeString("DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA")
                part_hash, _ := hex.DecodeString("0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF")
                proposal := &types.Proposal{
                    Type:     prototypes.SignedMsgType(prototypes.ProposalType),
                    Height:   12345,
                    Round:    23456,
                    POLRound: -1,
                    BlockID: types.BlockID{
                        Hash: block_hash,
                        PartSetHeader: types.PartSetHeader{
                            Hash:  part_hash,
                            Total: 65535,
                        },
                    },
                    Timestamp: stamp,
                }
                signBytes := types.ProposalSignBytes("test_chain_id", proposal.ToProto())
                fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v", signBytes), " "), ", "))
            }
        */

        let want = vec![
            136, 1, 8, 32, 17, 57, 48, 0, 0, 0, 0, 0, 0, 25, 160, 91, 0, 0, 0, 0, 0, 0, 32, 255,
            255, 255, 255, 255, 255, 255, 255, 255, 1, 42, 74, 10, 32, 222, 173, 190, 239, 222,
            173, 190, 239, 186, 251, 175, 186, 251, 175, 186, 250, 222, 173, 190, 239, 222, 173,
            190, 239, 186, 251, 175, 186, 251, 175, 186, 250, 18, 38, 8, 255, 255, 3, 18, 32, 0,
            34, 68, 102, 136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102,
            136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211,
            5, 16, 192, 242, 227, 236, 2, 58, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110,
            95, 105, 100,
        ];

        assert_eq!(got, want)
    }

    #[test]
    // Test proposal encoding with a malformed block ID which is considered null in Go.
    fn test_encoding_with_empty_block_id() {
        let dt = "2018-02-11T07:09:22.765Z".parse::<DateTime<Utc>>().unwrap();
        let proposal = Proposal {
            msg_type: Type::Proposal,
            height: Height::from(12345_u32),
            round: Round::from(23456_u16),
            pol_round: None,
            block_id: Some(BlockId {
                hash: Hash::from_hex_upper(Algorithm::Sha256, "").unwrap(),
                part_set_header: Header::new(
                    65535,
                    Hash::from_hex_upper(
                        Algorithm::Sha256,
                        "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
                    )
                    .unwrap(),
                )
                .unwrap(),
            }),
            timestamp: Some(dt.into()),
            signature: Signature::Ed25519(Ed25519Signature::new([0; ED25519_SIGNATURE_SIZE])),
        };

        let mut got = vec![];

        let request = SignProposalRequest {
            proposal,
            chain_id: ChainId::from_str("test_chain_id").unwrap(),
        };

        let _have = request.to_signable_bytes(&mut got);

        // the following vector is generated via:
        /*
            import (
                "encoding/hex"
                "fmt"
                prototypes "github.com/tendermint/tendermint/proto/tendermint/types"
                "github.com/tendermint/tendermint/types"
                "strings"
                "time"
            )

            func proposalSerialize() {
                stamp, _ := time.Parse(time.RFC3339Nano, "2018-02-11T07:09:22.765Z")
                block_hash, _ := hex.DecodeString("")
                part_hash, _ := hex.DecodeString("0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF")
                proposal := &types.Proposal{
                    Type:     prototypes.SignedMsgType(prototypes.ProposalType),
                    Height:   12345,
                    Round:    23456,
                    POLRound: -1,
                    BlockID: types.BlockID{
                        Hash: block_hash,
                        PartSetHeader: types.PartSetHeader{
                            Hash:  part_hash,
                            Total: 65535,
                        },
                    },
                    Timestamp: stamp,
                }
                signBytes := types.ProposalSignBytes("test_chain_id", proposal.ToProto())
                fmt.Println(strings.Join(strings.Split(fmt.Sprintf("%v", signBytes), " "), ", "))
            }
        */

        let want = vec![
            102, 8, 32, 17, 57, 48, 0, 0, 0, 0, 0, 0, 25, 160, 91, 0, 0, 0, 0, 0, 0, 32, 255, 255,
            255, 255, 255, 255, 255, 255, 255, 1, 42, 40, 18, 38, 8, 255, 255, 3, 18, 32, 0, 34,
            68, 102, 136, 170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102, 136,
            170, 204, 238, 17, 51, 85, 119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211, 5,
            16, 192, 242, 227, 236, 2, 58, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110, 95,
            105, 100,
        ];

        assert_eq!(got, want)
    }

    #[test]
    fn test_deserialization() {
        let dt = "2018-02-11T07:09:22.765Z".parse::<DateTime<Utc>>().unwrap();
        let proposal = Proposal {
            msg_type: Type::Proposal,
            height: Height::from(12345_u32),
            round: Round::from(23456_u16),
            timestamp: Some(dt.into()),

            pol_round: None,
            block_id: Some(BlockId {
                hash: Hash::from_hex_upper(
                    Algorithm::Sha256,
                    "DEADBEEFDEADBEEFBAFBAFBAFBAFBAFADEADBEEFDEADBEEFBAFBAFBAFBAFBAFA",
                )
                .unwrap(),
                part_set_header: Header::new(
                    65535,
                    Hash::from_hex_upper(
                        Algorithm::Sha256,
                        "0022446688AACCEE1133557799BBDDFF0022446688AACCEE1133557799BBDDFF",
                    )
                    .unwrap(),
                )
                .unwrap(),
            }),
            signature: Signature::Ed25519(Ed25519Signature::new([0; ED25519_SIGNATURE_SIZE])),
        };
        let want = SignProposalRequest {
            proposal,
            chain_id: ChainId::from_str("test_chain_id").unwrap(),
        };

        let data = vec![
            10, 176, 1, 8, 32, 16, 185, 96, 24, 160, 183, 1, 32, 255, 255, 255, 255, 255, 255, 255,
            255, 255, 1, 42, 74, 10, 32, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251, 175,
            186, 251, 175, 186, 250, 222, 173, 190, 239, 222, 173, 190, 239, 186, 251, 175, 186,
            251, 175, 186, 250, 18, 38, 8, 255, 255, 3, 18, 32, 0, 34, 68, 102, 136, 170, 204, 238,
            17, 51, 85, 119, 153, 187, 221, 255, 0, 34, 68, 102, 136, 170, 204, 238, 17, 51, 85,
            119, 153, 187, 221, 255, 50, 12, 8, 162, 216, 255, 211, 5, 16, 192, 242, 227, 236, 2,
            58, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 13, 116, 101, 115, 116, 95, 99, 104, 97, 105, 110, 95,
            105, 100,
        ];

        let have = SignProposalRequest::decode_vec(&data).unwrap();
        assert_eq!(have, want);
    }
}
