use crate::block::Nonce;
use crate::consensus::supply::Amount;
use crate::crypto::{Address, PublicKey, Signature};
use crate::crypto::{address_from_public_key, generate_keypair, sign};
use crate::transaction::{SignedTransaction, Transaction, TransactionError};

const TEST_FEE: u64 = 2;

fn address(byte: u8) -> Address {
    Address([byte; 20])
}

fn transaction() -> Transaction {
    Transaction::new(
        address(1),
        address(2),
        Amount(10),
        Amount(TEST_FEE),
        Nonce(7),
    )
}

fn signed_payload(from: Address, to: Address, amount: u64, nonce: u64) -> Transaction {
    Transaction::new(from, to, Amount(amount), Amount(TEST_FEE), Nonce(nonce))
}

#[test]
fn validates_basic_transaction_rules() {
    assert_eq!(transaction().validate(), Ok(()));

    let mut unsupported_version = transaction();
    unsupported_version.version += 1;
    assert_eq!(
        unsupported_version.validate(),
        Err(TransactionError::UnsupportedVersion)
    );

    let mut zero_amount = transaction();
    zero_amount.amount = Amount(0);
    assert_eq!(zero_amount.validate(), Err(TransactionError::ZeroAmount));

    let mut same_addresses = transaction();
    same_addresses.to = same_addresses.from;
    assert_eq!(
        same_addresses.validate(),
        Err(TransactionError::SameSenderAndRecipient)
    );
}

#[test]
fn allows_zero_fee_at_core_validation_layer() {
    let mut transaction = transaction();
    transaction.fee = Amount(0);

    assert_eq!(transaction.validate(), Ok(()));
}

#[test]
fn treats_transaction_timestamp_as_signed_metadata() {
    let now = 1_700_000_000;
    let valid = Transaction::new_at(
        address(1),
        address(2),
        Amount(10),
        Amount(TEST_FEE),
        Nonce(7),
        now,
    );
    assert_eq!(valid.validate_at(now), Ok(()));

    let old = Transaction::new_at(
        address(1),
        address(2),
        Amount(10),
        Amount(TEST_FEE),
        Nonce(7),
        now - 2 * 24 * 60 * 60,
    );
    assert_eq!(old.validate_at(now), Ok(()));

    let future = Transaction::new_at(
        address(1),
        address(2),
        Amount(10),
        Amount(TEST_FEE),
        Nonce(7),
        now + 2 * crate::consensus::BLOCK_TIME as u64,
    );
    assert_eq!(future.validate_at(now), Ok(()));
}

#[test]
fn signed_transaction_timestamp_policy_is_outside_core_validation() {
    let keypair = generate_keypair();
    let from = address_from_public_key(&keypair.public_key);
    let now = 1_700_000_000;
    let payload = Transaction::new_at(
        from,
        address(2),
        Amount(10),
        Amount(TEST_FEE),
        Nonce(0),
        now,
    );
    let signature = sign(&keypair.secret_key, &payload.signing_bytes());
    let signed = SignedTransaction::new(payload, keypair.public_key, signature);

    assert_eq!(signed.validate_signed_at(now), Ok(()));
}

#[test]
fn hashes_are_deterministic_and_change_with_payload() {
    let mut changed = transaction();
    changed.nonce = Nonce(8);

    assert_eq!(transaction().hash(), transaction().hash());
    assert_ne!(transaction().hash(), changed.hash());
}

#[test]
fn signed_transaction_requires_signature_material() {
    let signed = SignedTransaction::new(
        signed_payload(address(1), address(2), 10, 7),
        PublicKey([1; 2592]),
        Signature([1; 4627]),
    );

    assert_eq!(signed.validate(), Ok(()));
    assert_eq!(signed.transaction_hash(), signed.transaction.hash());
    assert!(signed.serialized_size() <= crate::transaction::MAX_TX_SIZE);

    let without_key = SignedTransaction::new(
        signed_payload(address(1), address(2), 10, 7),
        PublicKey([0; 2592]),
        Signature([1; 4627]),
    );
    assert_eq!(
        without_key.validate(),
        Err(TransactionError::EmptyPublicKey)
    );

    let without_signature = SignedTransaction::new(
        signed_payload(address(1), address(2), 10, 7),
        PublicKey([1; 2592]),
        Signature([0; 4627]),
    );
    assert_eq!(
        without_signature.validate(),
        Err(TransactionError::EmptySignature)
    );
}

#[test]
fn verifies_signed_transaction_signature_and_sender_address() {
    let keypair = generate_keypair();
    let from = address_from_public_key(&keypair.public_key);
    let payload = signed_payload(from, address(2), 10, 0);
    let signature = sign(&keypair.secret_key, &payload.signing_bytes());
    let signed = SignedTransaction::new(payload, keypair.public_key, signature);

    assert_eq!(signed.sender_address(), from);
    assert_eq!(signed.verify_signature(), Ok(()));
    assert_eq!(signed.validate_signed(), Ok(()));
}

#[test]
fn rejects_signature_without_transaction_domain() {
    let keypair = generate_keypair();
    let from = address_from_public_key(&keypair.public_key);
    let payload = signed_payload(from, address(2), 10, 0);
    let signature = sign(&keypair.secret_key, &payload.to_bytes());
    let signed = SignedTransaction::new(payload, keypair.public_key, signature);

    assert_eq!(
        signed.verify_signature(),
        Err(TransactionError::InvalidSignature)
    );
}

#[test]
fn rejects_signed_transaction_with_wrong_sender_address() {
    let keypair = generate_keypair();
    let payload = signed_payload(address(1), address(2), 10, 0);
    let signature = sign(&keypair.secret_key, &payload.signing_bytes());
    let signed = SignedTransaction::new(payload, keypair.public_key, signature);

    assert_eq!(
        signed.validate_signed(),
        Err(TransactionError::SenderAddressMismatch)
    );
}

#[test]
fn rejects_signed_transaction_with_invalid_signature() {
    let keypair = generate_keypair();
    let from = address_from_public_key(&keypair.public_key);
    let payload = signed_payload(from, address(2), 10, 0);
    let mut signed = SignedTransaction::new(
        payload.clone(),
        keypair.public_key,
        sign(&keypair.secret_key, &payload.signing_bytes()),
    );
    signed.witness.signature.0[0] ^= 0xff;

    assert_eq!(
        signed.validate_signed(),
        Err(TransactionError::InvalidSignature)
    );
}
