use {super::*, splitfile::Splitfile};

mod splitfile;

#[derive(Debug, PartialEq)]
enum Error {
  DustOutput {
    value: Amount,
    threshold: Amount,
    output: usize,
  },
  DustPostage {
    value: Amount,
    threshold: Amount,
  },
  NoOutputs,
  RunestoneSize {
    size: usize,
  },
  Shortfall {
    rune: SpacedRune,
    have: Pile,
    need: Pile,
  },
  ZeroValue {
    output: usize,
    rune: SpacedRune,
  },
}

impl Display for Error {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Self::DustOutput {
        value,
        threshold,
        output,
      } => write!(
        f,
        "output {output} value {value} below dust threshold {threshold}"
      ),
      Self::DustPostage { value, threshold } => {
        write!(f, "postage value {value} below dust threshold {threshold}")
      }
      Self::NoOutputs => write!(f, "split file must contain at least one output"),
      Self::RunestoneSize { size } => write!(
        f,
        "runestone size {size} over maximum standard OP_RETURN size {MAX_STANDARD_OP_RETURN_SIZE}"
      ),
      Self::Shortfall { rune, have, need } => {
        write!(f, "wallet contains {have} of {rune} but need {need}")
      }
      Self::ZeroValue { output, rune } => {
        write!(f, "output {output} has zero value for rune {rune}")
      }
    }
  }
}

impl std::error::Error for Error {}

#[derive(Debug, Parser)]
pub(crate) struct Split {
  #[arg(long, help = "Don't sign or broadcast transaction")]
  pub(crate) dry_run: bool,
  #[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB")]
  fee_rate: FeeRate,
  #[arg(
    long,
    help = "Include <AMOUNT> postage with change output. [default: 10000 sat]",
    value_name = "AMOUNT"
  )]
  pub(crate) postage: Option<Amount>,
  #[arg(
    long,
    help = "Split outputs multiple inscriptions and rune defined in YAML <SPLIT_FILE>.",
    value_name = "SPLIT_FILE"
  )]
  pub(crate) splits: PathBuf,
  #[arg(
    long,
    alias = "nolimit",
    help = "Allow OP_RETURN greater than 83 bytes. Transactions over this limit are nonstandard \
    and will not be relayed by bitcoind in its default configuration. Do not use this flag unless \
    you understand the implications."
  )]
  pub(crate) no_limit: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Output {
  pub txid: Txid,
  pub psbt: String,
  pub fee: u64,
}

impl Split {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    ensure!(
      wallet.has_rune_index(),
      "`ord wallet split` requires index created with `--index-runes`",
    );

    wallet.lock_non_cardinal_outputs()?;

    let splits = Splitfile::load(&self.splits, &wallet)?;

    let inscribed_outputs = wallet
      .inscriptions()
      .keys()
      .map(|satpoint| satpoint.outpoint)
      .collect::<HashSet<OutPoint>>();

    let balances = wallet
      .get_runic_outputs()?
      .unwrap_or_default()
      .into_iter()
      .filter(|output| !inscribed_outputs.contains(output))
      .map(|output| {
        wallet.get_runes_balances_in_output(&output).map(|balance| {
          (
            output,
            balance
              .unwrap_or_default()
              .into_iter()
              .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount))
              .collect(),
          )
        })
      })
      .collect::<Result<BTreeMap<OutPoint, BTreeMap<Rune, u128>>>>()?;

    let unfunded_transaction = Self::build_transaction(
      self.no_limit,
      balances,
      &wallet.get_change_address()?,
      self.postage,
      &splits,
    )?;

    let unsigned_transaction = fund_raw_transaction(
      wallet.bitcoin_client(),
      self.fee_rate,
      &unfunded_transaction,
      None,
    )?;

    let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?;

    let (txid, psbt, fee) =
      wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run, None)?;

    Ok(Some(Box::new(Output { txid, psbt, fee })))
  }

  fn build_transaction(
    no_runestone_limit: bool,
    balances: BTreeMap<OutPoint, BTreeMap<Rune, u128>>,
    change_address: &Address,
    postage: Option<Amount>,
    splits: &Splitfile,
  ) -> Result<Transaction, Error> {
    if splits.outputs.is_empty() {
      return Err(Error::NoOutputs);
    }

    let postage = postage.unwrap_or(TARGET_POSTAGE);

    let change_script_pubkey = change_address.script_pubkey();

    let change_dust_threshold = change_script_pubkey.minimal_non_dust();

    if postage < change_script_pubkey.minimal_non_dust() {
      return Err(Error::DustPostage {
        value: postage,
        threshold: change_dust_threshold,
      });
    }

    let mut input_runes_required = BTreeMap::<Rune, u128>::new();

    for (i, output) in splits.outputs.iter().enumerate() {
      for (&rune, &amount) in &output.runes {
        if amount == 0 {
          return Err(Error::ZeroValue {
            rune: splits.rune_info[&rune].spaced_rune,
            output: i,
          });
        }
        let required = input_runes_required.entry(rune).or_default();
        *required = (*required).checked_add(amount).unwrap();
      }
    }

    let mut input_rune_balances: BTreeMap<Rune, u128> = BTreeMap::new();

    let mut inputs = Vec::new();

    for (output, runes) in balances {
      for (rune, required) in &input_runes_required {
        if input_rune_balances.get(rune).copied().unwrap_or_default() >= *required {
          continue;
        }

        if runes.get(rune).copied().unwrap_or_default() == 0 {
          continue;
        }

        for (rune, balance) in &runes {
          *input_rune_balances.entry(*rune).or_default() += balance;
        }

        inputs.push(output);

        break;
      }
    }

    for (&rune, &need) in &input_runes_required {
      let have = input_rune_balances.get(&rune).copied().unwrap_or_default();
      if have < need {
        let info = splits.rune_info[&rune];
        return Err(Error::Shortfall {
          rune: info.spaced_rune,
          have: Pile {
            amount: have,
            divisibility: info.divisibility,
            symbol: info.symbol,
          },
          need: Pile {
            amount: need,
            divisibility: info.divisibility,
            symbol: info.symbol,
          },
        });
      }
    }

    let mut need_rune_change_output = false;
    for (rune, input) in input_rune_balances {
      if input > input_runes_required.get(&rune).copied().unwrap_or_default() {
        need_rune_change_output = true;
      }
    }

    let mut edicts = Vec::new();

    let base = if need_rune_change_output { 2 } else { 1 };

    for (i, output) in splits.outputs.iter().enumerate() {
      for (rune, amount) in &output.runes {
        edicts.push(Edict {
          id: splits.rune_info.get(rune).unwrap().id,
          amount: *amount,
          output: (i + base).try_into().unwrap(),
        });
      }
    }

    let runestone = Runestone {
      edicts,
      ..default()
    };

    let mut output = Vec::new();

    let runestone_script_pubkey = runestone.encipher();
    let size = runestone_script_pubkey.len();

    if !no_runestone_limit && size > MAX_STANDARD_OP_RETURN_SIZE {
      return Err(Error::RunestoneSize { size });
    }

    output.push(TxOut {
      script_pubkey: runestone_script_pubkey,
      value: Amount::from_sat(0),
    });

    if need_rune_change_output {
      output.push(TxOut {
        script_pubkey: change_script_pubkey,
        value: postage,
      });
    }

    for (i, split_output) in splits.outputs.iter().enumerate() {
      let script_pubkey = split_output.address.script_pubkey();
      let threshold = script_pubkey.minimal_non_dust();
      let value = split_output.value.unwrap_or(threshold);
      if value < threshold {
        return Err(Error::DustOutput {
          output: i,
          threshold,
          value,
        });
      }
      output.push(TxOut {
        script_pubkey,
        value,
      });
    }

    let tx = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: inputs
        .into_iter()
        .map(|previous_output| TxIn {
          previous_output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        })
        .collect(),
      output,
    };

    for output in &tx.output {
      assert!(output.value >= output.script_pubkey.minimal_non_dust());
    }

    assert_eq!(
      Runestone::decipher(&tx),
      Some(Artifact::Runestone(runestone)),
    );

    Ok(tx)
  }
}

#[cfg(test)]
mod tests {
  use {super::*, splitfile::RuneInfo};

  #[test]
  fn splits_must_have_at_least_one_output() {
    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        None,
        &Splitfile {
          outputs: Vec::new(),
          rune_info: BTreeMap::new(),
        },
      )
      .unwrap_err(),
      Error::NoOutputs,
    );
  }

  #[test]
  fn postage_may_not_be_dust() {
    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        Some(Amount::from_sat(100)),
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 1000)].into(),
            value: Some(Amount::from_sat(1000)),
          }],
          rune_info: BTreeMap::new(),
        },
      )
      .unwrap_err(),
      Error::DustPostage {
        value: Amount::from_sat(100),
        threshold: Amount::from_sat(294),
      },
    );
  }

  #[test]
  fn output_rune_value_may_not_be_zero() {
    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 0)].into(),
            value: Some(Amount::from_sat(1000)),
          }],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 10,
              symbol: Some('@'),
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 1,
              },
            },
          )]
          .into()
        },
      )
      .unwrap_err(),
      Error::ZeroValue {
        output: 0,
        rune: SpacedRune {
          rune: Rune(0),
          spacers: 1,
        },
      },
    );

    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![
            splitfile::Output {
              address: address(0),
              runes: [(Rune(0), 100)].into(),
              value: Some(Amount::from_sat(1000)),
            },
            splitfile::Output {
              address: address(0),
              runes: [(Rune(0), 0)].into(),
              value: Some(Amount::from_sat(1000)),
            },
          ],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 10,
              symbol: Some('@'),
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 10,
              },
            },
          )]
          .into()
        },
      )
      .unwrap_err(),
      Error::ZeroValue {
        output: 1,
        rune: SpacedRune {
          rune: Rune(0),
          spacers: 10,
        },
      },
    );
  }

  #[test]
  fn wallet_must_have_enough_runes() {
    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 1000)].into(),
            value: Some(Amount::from_sat(1000)),
          }],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 10,
              symbol: Some('@'),
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 2,
              },
            },
          )]
          .into(),
        },
      )
      .unwrap_err(),
      Error::Shortfall {
        rune: SpacedRune {
          rune: Rune(0),
          spacers: 2
        },
        have: Pile {
          amount: 0,
          divisibility: 10,
          symbol: Some('@'),
        },
        need: Pile {
          amount: 1000,
          divisibility: 10,
          symbol: Some('@'),
        },
      },
    );

    assert_eq!(
      Split::build_transaction(
        false,
        [(outpoint(0), [(Rune(0), 1000)].into())].into(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 2000)].into(),
            value: Some(Amount::from_sat(1000)),
          }],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 2,
              symbol: Some('x'),
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 1
              },
            },
          )]
          .into()
        },
      )
      .unwrap_err(),
      Error::Shortfall {
        rune: SpacedRune {
          rune: Rune(0),
          spacers: 1,
        },
        have: Pile {
          amount: 1000,
          divisibility: 2,
          symbol: Some('x'),
        },
        need: Pile {
          amount: 2000,
          divisibility: 2,
          symbol: Some('x'),
        },
      },
    );
  }

  #[test]
  fn split_output_values_may_not_be_dust() {
    assert_eq!(
      Split::build_transaction(
        false,
        [(outpoint(0), [(Rune(0), 1000)].into())].into(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 1000)].into(),
            value: Some(Amount::from_sat(1)),
          }],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 0,
              symbol: None,
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 0,
              },
            },
          )]
          .into(),
        },
      )
      .unwrap_err(),
      Error::DustOutput {
        value: Amount::from_sat(1),
        threshold: Amount::from_sat(294),
        output: 0,
      }
    );

    assert_eq!(
      Split::build_transaction(
        false,
        [(outpoint(0), [(Rune(0), 2000)].into())].into(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![
            splitfile::Output {
              address: address(0),
              runes: [(Rune(0), 1000)].into(),
              value: Some(Amount::from_sat(1000)),
            },
            splitfile::Output {
              address: address(0),
              runes: [(Rune(0), 1000)].into(),
              value: Some(Amount::from_sat(10)),
            },
          ],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 0,
              symbol: None,
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 0,
              },
            },
          )]
          .into()
        },
      )
      .unwrap_err(),
      Error::DustOutput {
        value: Amount::from_sat(10),
        threshold: Amount::from_sat(294),
        output: 1,
      }
    );
  }

  #[test]
  fn one_output_no_change() {
    let address = address(0);
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [(output, [(rune, 1000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 1
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn one_output_with_change_for_outgoing_rune_with_default_postage() {
    let address = address(0);
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };
    let change = change(0);

    let balances = [(output, [(rune, 2000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change, None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 2
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: change.into(),
            value: TARGET_POSTAGE,
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn one_output_with_change_for_outgoing_rune_with_non_default_postage() {
    let address = address(0);
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };
    let change = change(0);

    let balances = [(output, [(rune, 2000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(
      false,
      balances,
      &change,
      Some(Amount::from_sat(500)),
      &splits,
    )
    .unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 2
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: change.into(),
            value: Amount::from_sat(500),
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn one_output_with_change_for_non_outgoing_rune() {
    let address = address(0);
    let output = outpoint(0);
    let change = change(0);

    let balances = [(output, [(Rune(0), 1000), (Rune(1), 1000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(Rune(0), 1000)].into(),
        value: None,
      }],
      rune_info: [(
        Rune(0),
        RuneInfo {
          id: rune_id(0),
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change, None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id: rune_id(0),
                amount: 1000,
                output: 2
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: change.into(),
            value: TARGET_POSTAGE,
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn outputs_without_value_use_correct_dust_amount() {
    let address = "bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297"
      .parse::<Address<NetworkUnchecked>>()
      .unwrap()
      .assume_checked();
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [(output, [(rune, 1000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 1
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(330),
          }
        ],
      },
    );
  }

  #[test]
  fn excessive_inputs_are_not_selected() {
    let address = address(0);
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [
      (output, [(rune, 1000)].into()),
      (outpoint(1), [(rune, 1000)].into()),
    ]
    .into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 1
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn multiple_inputs_may_be_selected() {
    let address = address(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [
      (outpoint(0), [(rune, 1000)].into()),
      (outpoint(1), [(rune, 1000)].into()),
    ]
    .into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 2000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![
          TxIn {
            previous_output: outpoint(0),
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          },
          TxIn {
            previous_output: outpoint(1),
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          },
        ],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 2000,
                output: 1
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn two_outputs_no_change() {
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [(output, [(rune, 1000)].into())].into();

    let splits = Splitfile {
      outputs: vec![
        splitfile::Output {
          address: address(0),
          runes: [(rune, 500)].into(),
          value: None,
        },
        splitfile::Output {
          address: address(1),
          runes: [(rune, 500)].into(),
          value: None,
        },
      ],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![
                Edict {
                  id,
                  amount: 500,
                  output: 1
                },
                Edict {
                  id,
                  amount: 500,
                  output: 2
                }
              ],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address(0).into(),
            value: Amount::from_sat(294),
          },
          TxOut {
            script_pubkey: address(1).into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn outputs_may_receive_multiple_runes() {
    let address = address(0);

    let balances = [
      (outpoint(0), [(Rune(0), 1000)].into()),
      (outpoint(1), [(Rune(1), 2000)].into()),
    ]
    .into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(Rune(0), 1000), (Rune(1), 2000)].into(),
        value: None,
      }],
      rune_info: [
        (
          Rune(0),
          RuneInfo {
            id: rune_id(0),
            divisibility: 0,
            symbol: None,
            spaced_rune: SpacedRune {
              rune: Rune(0),
              spacers: 0,
            },
          },
        ),
        (
          Rune(1),
          RuneInfo {
            id: rune_id(1),
            divisibility: 0,
            symbol: None,
            spaced_rune: SpacedRune {
              rune: Rune(1),
              spacers: 0,
            },
          },
        ),
      ]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![
          TxIn {
            previous_output: outpoint(0),
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          },
          TxIn {
            previous_output: outpoint(1),
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          },
        ],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![
                Edict {
                  id: rune_id(0),
                  amount: 1000,
                  output: 1
                },
                Edict {
                  id: rune_id(1),
                  amount: 2000,
                  output: 1
                },
              ],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn oversize_op_return_is_an_error() {
    let balances = [(outpoint(0), [(Rune(0), 10_000_000_000)].into())].into();

    let splits = Splitfile {
      outputs: (0..10)
        .map(|i| splitfile::Output {
          address: address(i).clone(),
          runes: [(Rune(0), 1_000_000_000)].into(),
          value: None,
        })
        .collect(),
      rune_info: [(
        Rune(0),
        RuneInfo {
          id: rune_id(0),
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    assert_eq!(
      Split::build_transaction(false, balances, &change(0), None, &splits).unwrap_err(),
      Error::RunestoneSize { size: 85 },
    );
  }

  #[test]
  fn oversize_op_return_is_allowed_with_flag() {
    let balances = [(outpoint(0), [(Rune(0), 10_000_000_000)].into())].into();

    let splits = Splitfile {
      outputs: (0..10)
        .map(|i| splitfile::Output {
          address: address(i).clone(),
          runes: [(Rune(0), 1_000_000_000)].into(),
          value: None,
        })
        .collect(),
      rune_info: [(
        Rune(0),
        RuneInfo {
          id: rune_id(0),
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    pretty_assert_eq!(
      Split::build_transaction(true, balances, &change(0), None, &splits).unwrap(),
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: outpoint(0),
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: (0..11)
          .map(|i| if i == 0 {
            TxOut {
              value: Amount::from_sat(0),
              script_pubkey: Runestone {
                edicts: (0..10)
                  .map(|i| Edict {
                    id: rune_id(0),
                    amount: 1_000_000_000,
                    output: i + 1,
                  })
                  .collect(),
                etching: None,
                mint: None,
                pointer: None,
              }
              .encipher(),
            }
          } else {
            TxOut {
              script_pubkey: address(i - 1).into(),
              value: Amount::from_sat(294),
            }
          })
          .collect()
      }
    );
  }
}
