use {
  super::*,
  ord::{
    Attributes, Item, Properties, Trait, Traits, decimal::Decimal, subcommand::wallet::send,
    templates::ItemHtml,
  },
  pretty_assertions::assert_eq,
  std::io::Read,
};

fn receive(core: &mockcore::Handle, ord: &TestServer) -> Address {
  let address = CommandBuilder::new("wallet receive")
    .core(core)
    .ord(ord)
    .run_and_deserialize_output::<ord::subcommand::wallet::receive::Output>()
    .addresses
    .into_iter()
    .next()
    .unwrap();

  address.require_network(core.state().network).unwrap()
}

#[test]
fn batch_inscribe_fails_if_batchfile_has_no_inscriptions() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("batch.yaml", "mode: shared-output\ninscriptions: []\n")
    .core(&core)
    .ord(&ord)
    .stderr_regex(".*batchfile must contain at least one inscription.*")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn batch_inscribe_can_create_one_inscription() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write(
      "batch.yaml",
      "
mode: shared-output
inscriptions:
- file: inscription.txt
  title: bar
  traits:
    foo: true
    bar: null
    baz: 67
    qux: hello
  metadata: 123
  metaprotocol: foo
",
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(core.descriptors().len(), 3);

  let request = ord.request(format!("/content/{}", output.inscriptions[0].id));

  assert_eq!(request.status(), 200);
  assert_eq!(
    request.headers().get("content-type").unwrap(),
    "text/plain;charset=utf-8"
  );
  assert_eq!(request.text().unwrap(), "Hello World");

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    concat!(
      r".*",
      r"<dt>title</dt>\s*",
      r"<dd>bar</dd>\s*",
      r"<dt>traits</dt>\s*",
      r"<dd>\s*",
      r"<dl>\s*",
      r"<dt>foo</dt><dd>true</dd>\s*",
      r"<dt>bar</dt><dd>null</dd>\s*",
      r"<dt>baz</dt><dd>67</dd>\s*",
      r"<dt>qux</dt><dd>hello</dd>\s*",
      r"</dl>\s*",
      r"</dd>\s*",
      r"<dt>metadata</dt>\s*",
      r"<dd>\n    123\n  </dd>.*",
      r"<dt>metaprotocol</dt>\s*",
      r"<dd>foo</dd>.*",
    ),
  );
}

#[test]
fn trait_names_may_not_be_duplicated() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write(
      "batch.yaml",
      "
mode: shared-output
inscriptions:
- file: inscription.txt
  title: bar
  traits:
    foo: true
    foo: null
  metadata: 123
  metaprotocol: foo
",
    )
    .core(&core)
    .ord(&ord)
    .stderr_regex(r"error: inscriptions\[0\]: duplicate trait foo at line 4 column 3\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn batch_inscribe_with_multiple_inscriptions() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet batch --batch batch.yaml --fee-rate 55")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      "mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n"
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(core.descriptors().len(), 3);

  let request = ord.request(format!("/content/{}", output.inscriptions[0].id));
  assert_eq!(request.status(), 200);
  assert_eq!(
    request.headers().get("content-type").unwrap(),
    "text/plain;charset=utf-8"
  );
  assert_eq!(request.text().unwrap(), "Hello World");

  let request = ord.request(format!("/content/{}", output.inscriptions[1].id));
  assert_eq!(request.status(), 200);
  assert_eq!(request.headers().get("content-type").unwrap(), "image/png");

  let request = ord.request(format!("/content/{}", output.inscriptions[2].id));
  assert_eq!(request.status(), 200);
  assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav");
}

#[test]
fn batch_inscribe_with_multiple_inscriptions_with_parent() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(core.descriptors().len(), 3);

  let parent_id = parent_output.inscriptions[0].id;

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("parents:\n- {parent_id}\nmode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    r".*<dt>parents</dt>\s*<dd>.*</dd>.*",
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    r".*<dt>parents</dt>\s*<dd>.*</dd>.*",
  );

  let request = ord.request(format!("/content/{}", output.inscriptions[2].id));
  assert_eq!(request.status(), 200);
  assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav");
}

#[test]
fn batch_inscribe_inscriptions_with_multiple_parents() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let parent_output_1 = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  let parent_output_2 = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  let parent_output_3 = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  let parent_id_1 = parent_output_1.inscriptions[0].id;
  let parent_id_2 = parent_output_2.inscriptions[0].id;
  let parent_id_3 = parent_output_3.inscriptions[0].id;

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("parents:\n- {parent_id_1}\n- {parent_id_2}\n- {parent_id_3}\nmode: separate-outputs\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(r".*<dt>parents</dt>\s*<dd>.*{parent_id_1}.*{parent_id_2}.*{parent_id_3}.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(r".*<dt>parents</dt>\s*<dd>.*{parent_id_1}.*{parent_id_2}.*{parent_id_3}.*",),
  );
}

#[test]
fn batch_inscribe_and_etch_with_two_parents() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let parent_output_1 = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  let parent_output_2 = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(core.descriptors().len(), 4);

  let parent_id_1 = parent_output_1.inscriptions[0].id;
  let parent_id_2 = parent_output_2.inscriptions[0].id;

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("parents:\n- {parent_id_1}\n- {parent_id_2}\nmode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(r".*<dt>parents</dt>\s*<dd>.*{parent_id_1}.*{parent_id_2}.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(r".*<dt>parents</dt>\s*<dd>.*{parent_id_1}.*{parent_id_2}.*",),
  );

  let request = ord.request(format!("/content/{}", output.inscriptions[2].id));
  assert_eq!(request.status(), 200);
  assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav");
}

#[test]
fn batch_inscribe_respects_dry_run_flag() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml --dry-run")
    .write("inscription.txt", "Hello World")
    .write(
      "batch.yaml",
      "mode: shared-output\ninscriptions:\n- file: inscription.txt\n",
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert!(core.mempool().is_empty());

  let request = ord.request(format!("/content/{}", output.inscriptions[0].id));

  assert_eq!(request.status(), 404);
}

#[test]
fn batch_in_same_output_but_different_satpoints() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      "mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n"
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  let outpoint = output.inscriptions[0].location.outpoint;
  for (i, inscription) in output.inscriptions.iter().enumerate() {
    assert_eq!(
      inscription.location,
      SatPoint {
        outpoint,
        offset: u64::try_from(i).unwrap() * 10_000,
      }
    );
  }

  core.mine_blocks(1);

  let outpoint = output.inscriptions[0].location.outpoint;

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(r".*<dt>location</dt>.*{outpoint}:10000.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[2].id),
    format!(r".*<dt>location</dt>.*{outpoint}:20000.*",),
  );

  ord.assert_response_regex(
    format!("/output/{}", output.inscriptions[0].location.outpoint),
    format!(r".*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id),
  );
}

#[test]
fn batch_in_same_output_with_non_default_postage() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      "mode: shared-output\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n"
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  let outpoint = output.inscriptions[0].location.outpoint;

  for (i, inscription) in output.inscriptions.iter().enumerate() {
    assert_eq!(
      inscription.location,
      SatPoint {
        outpoint,
        offset: u64::try_from(i).unwrap() * 777,
      }
    );
  }

  core.mine_blocks(1);

  let outpoint = output.inscriptions[0].location.outpoint;

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(r".*<dt>location</dt>.*{outpoint}:777.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[2].id),
    format!(r".*<dt>location</dt>.*{outpoint}:1554.*",),
  );

  ord.assert_response_regex(
    format!("/output/{}", output.inscriptions[0].location.outpoint),
    format!(r".*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id),
  );
}

#[test]
fn batch_in_separate_outputs_with_parent() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(core.descriptors().len(), 3);

  let parent_id = parent_output.inscriptions[0].id;

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("parents:\n- {parent_id}\nmode: separate-outputs\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  for inscription in &output.inscriptions {
    assert_eq!(inscription.location.offset, 0);
  }

  let mut outpoints = output
    .inscriptions
    .iter()
    .map(|inscription| inscription.location.outpoint)
    .collect::<Vec<OutPoint>>();
  outpoints.sort();
  outpoints.dedup();
  assert_eq!(outpoints.len(), output.inscriptions.len());

  core.mine_blocks(1);

  let output_1 = output.inscriptions[0].location.outpoint;
  let output_2 = output.inscriptions[1].location.outpoint;
  let output_3 = output.inscriptions[2].location.outpoint;

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(
      r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>10000</dd>.*.*<dt>location</dt>.*{output_1}:0.*"
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(
      r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>10000</dd>.*.*<dt>location</dt>.*{output_2}:0.*"
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[2].id),
    format!(
      r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>10000</dd>.*.*<dt>location</dt>.*{output_3}:0.*"
    ),
  );
}

#[test]
fn batch_in_separate_outputs_with_parent_and_non_default_postage() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(core.descriptors().len(), 3);

  let parent_id = parent_output.inscriptions[0].id;

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("parents:\n- {parent_id}\nmode: separate-outputs\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  for inscription in &output.inscriptions {
    assert_eq!(inscription.location.offset, 0);
  }

  let mut outpoints = output
    .inscriptions
    .iter()
    .map(|inscription| inscription.location.outpoint)
    .collect::<Vec<OutPoint>>();
  outpoints.sort();
  outpoints.dedup();
  assert_eq!(outpoints.len(), output.inscriptions.len());

  core.mine_blocks(1);

  let output_1 = output.inscriptions[0].location.outpoint;
  let output_2 = output.inscriptions[1].location.outpoint;
  let output_3 = output.inscriptions[2].location.outpoint;

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(
      r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>777</dd>.*.*<dt>location</dt>.*{output_1}:0.*"
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(
      r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>777</dd>.*.*<dt>location</dt>.*{output_2}:0.*"
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[2].id),
    format!(
      r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>777</dd>.*.*<dt>location</dt>.*{output_3}:0.*"
    ),
  );
}

#[test]
fn batch_inscribe_fails_if_invalid_network_destination_address() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest wallet batch --fee-rate 2.1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("batch.yaml", "mode: separate-outputs\ninscriptions:\n- file: inscription.txt\n  destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
    .core(&core)
    .ord(&ord)
    .stderr_regex("error: validation error\n\nbecause:\n- address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 is not valid on regtest\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn batch_inscribe_fails_with_shared_output_or_same_sat_and_destination_set() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", "")
    .write("batch.yaml", "mode: shared-output\ninscriptions:\n- file: inscription.txt\n  destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png")
    .core(&core)
    .ord(&ord)
    .expected_exit_code(1)
    .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n")
    .run_and_extract_stdout();

  CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", "")
    .write("batch.yaml", "mode: same-sat\nsat: 5000000000\ninscriptions:\n- file: inscription.txt\n  destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png")
    .core(&core)
    .ord(&ord)
    .expected_exit_code(1)
    .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n")
    .run_and_extract_stdout();
}

#[test]
fn batch_inscribe_works_with_some_destinations_set_and_others_not() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet batch --batch batch.yaml --fee-rate 55")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      "\
mode: separate-outputs
inscriptions:
- file: inscription.txt
  destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
- file: tulip.png
- file: meow.wav
  destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k
",
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(core.descriptors().len(), 3);

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    ".*
  <dt>address</dt>
  <dd><a class=collapse href=/address/bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4>bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4</a></dd>.*",
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(
      ".*
  <dt>address</dt>
  <dd><a class=collapse href=/address/{0}>{0}</a></dd>.*",
      core.state().change_addresses.iter().next().unwrap(),
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[2].id),
    ".*
  <dt>address</dt>
  <dd><a class=collapse href=/address/bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k>bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k</a></dd>.*",
  );
}

#[test]
fn batch_same_sat() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      "mode: same-sat\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n"
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  assert_eq!(
    output.inscriptions[0].location,
    output.inscriptions[1].location
  );
  assert_eq!(
    output.inscriptions[1].location,
    output.inscriptions[2].location
  );

  core.mine_blocks(1);

  let outpoint = output.inscriptions[0].location.outpoint;

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[2].id),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/output/{}", output.inscriptions[0].location.outpoint),
    format!(r".*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id),
  );
}

#[test]
fn batch_same_sat_with_parent() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  let parent_id = parent_output.inscriptions[0].id;

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("mode: same-sat\nparents:\n- {parent_id}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  assert_eq!(
    output.inscriptions[0].location,
    output.inscriptions[1].location
  );
  assert_eq!(
    output.inscriptions[1].location,
    output.inscriptions[2].location
  );

  core.mine_blocks(1);

  let txid = output.inscriptions[0].location.outpoint.txid;

  ord.assert_response_regex(
    format!("/inscription/{parent_id}"),
    format!(r".*<dt>location</dt>.*{txid}:0:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(r".*<dt>location</dt>.*{txid}:1:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(r".*<dt>location</dt>.*{txid}:1:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[2].id),
    format!(r".*<dt>location</dt>.*{txid}:1:0.*",),
  );

  ord.assert_response_regex(
    format!("/output/{}", output.inscriptions[0].location.outpoint),
    format!(r".*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id),
  );
}

#[test]
fn batch_same_sat_with_satpoint_and_reinscription() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png")
    .write("parent.png", [1; 520])
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  let inscription_id = output.inscriptions[0].id;
  let satpoint = output.inscriptions[0].location;

  CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("mode: same-sat\nsatpoint: {satpoint}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .expected_exit_code(1)
    .stderr_regex(".*error: sat at .*:0:0 already inscribed.*")
    .run_and_extract_stdout();

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("mode: same-sat\nsatpoint: {satpoint}\nreinscribe: true\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  assert_eq!(
    output.inscriptions[0].location,
    output.inscriptions[1].location
  );
  assert_eq!(
    output.inscriptions[1].location,
    output.inscriptions[2].location
  );

  core.mine_blocks(1);

  let outpoint = output.inscriptions[0].location.outpoint;

  ord.assert_response_regex(
    format!("/inscription/{inscription_id}"),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[0].id),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[1].id),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", output.inscriptions[2].id),
    format!(r".*<dt>location</dt>.*{outpoint}:0.*",),
  );

  ord.assert_response_regex(
    format!("/output/{}", output.inscriptions[0].location.outpoint),
    format!(r".*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*<a href=/inscription/{}>.*</a>.*", inscription_id, output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id),
  );
}

#[test]
fn batch_inscribe_with_sat_argument_with_parent() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let parent_output =
    CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png")
      .write("parent.png", [1; 520])
      .core(&core)
      .ord(&ord)
      .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(core.descriptors().len(), 3);

  let parent_id = parent_output.inscriptions[0].id;

  let output = CommandBuilder::new("--index-sats wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("parents:\n- {parent_id}\nmode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n")
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  ord.assert_response_regex(
    "/sat/5000111111",
    format!(
      ".*<a href=/inscription/{}>.*<a href=/inscription/{}>.*<a href=/inscription/{}>.*",
      output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id
    ),
  );
}

#[test]
fn batch_inscribe_with_sat_arg_fails_if_wrong_mode() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      "mode: shared-output\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n"
    )
    .core(&core)
    .ord(&ord)
    .expected_exit_code(1)
    .expected_stderr("error: `sat` or `satpoint` can only be set in `same-sat` mode\n")
    .run_and_extract_stdout();
}

#[test]
fn batch_inscribe_with_satpoint() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]);

  create_wallet(&core, &ord);

  let txid = core.mine_blocks(1)[0].txdata[0].compute_txid();

  let output = CommandBuilder::new("wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!("mode: same-sat\nsatpoint: {txid}:0:55555\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", )
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  ord.assert_response_regex(
    "/sat/5000055555",
    format!(
      ".*<a href=/inscription/{}>.*<a href=/inscription/{}>.*<a href=/inscription/{}>.*",
      output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id
    ),
  );
}

#[test]
fn batch_inscribe_with_fee_rate() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(2);

  let set_fee_rate = 1.0;

  let output = CommandBuilder::new(format!("--index-sats wallet batch --fee-rate {set_fee_rate} --batch batch.yaml"))
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      "mode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n"
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  let commit_tx = &core.mempool()[0];
  let mut fee = Amount::ZERO;
  for input in &commit_tx.input {
    fee += core.get_utxo_amount(&input.previous_output).unwrap();
  }
  for output in &commit_tx.output {
    fee -= output.value;
  }
  let fee_rate = fee.to_sat() as f64 / commit_tx.vsize() as f64;
  pretty_assert_eq!(fee_rate, set_fee_rate);

  let reveal_tx = &core.mempool()[1];
  let mut fee = Amount::ZERO;
  for input in &reveal_tx.input {
    fee += commit_tx.output[input.previous_output.vout as usize].value;
  }
  for output in &reveal_tx.output {
    fee -= output.value;
  }
  let fee_rate = fee.to_sat() as f64 / reveal_tx.vsize() as f64;
  pretty_assert_eq!(fee_rate, set_fee_rate);

  assert_eq!(
    ord::FeeRate::try_from(set_fee_rate)
      .unwrap()
      .fee(commit_tx.vsize() + reveal_tx.vsize())
      .to_sat(),
    output.total_fees
  );
}

#[test]
fn batch_inscribe_with_delegate_inscription() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let (delegate, _) = inscribe(&core, &ord);

  let inscribe = CommandBuilder::new("wallet batch --fee-rate 1.0 --batch batch.yaml")
    .write("inscription.txt", "INSCRIPTION")
    .write(
      "batch.yaml",
      format!(
        "mode: shared-output
inscriptions:
- delegate: {delegate}
"
      ),
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  ord.assert_response_regex(
    format!("/inscription/{}", inscribe.inscriptions[0].id),
    format!(r#".*<dt>delegate</dt>\s*<dd><a href=/inscription/{delegate}>{delegate}</a></dd>.*"#,),
  );

  ord.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO");
}

#[test]
fn batch_inscribe_with_non_existent_delegate_inscription() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let delegate = "0000000000000000000000000000000000000000000000000000000000000000i0";

  CommandBuilder::new("wallet batch --fee-rate 1.0 --batch batch.yaml")
    .write("hello.txt", "Hello, world!")
    .write(
      "batch.yaml",
      format!(
        "mode: shared-output
inscriptions:
- delegate: {delegate}
  file: hello.txt
"
      ),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr(format!(
      "error: inscriptions referenced in batchfile do not exist: {delegate}\n"
    ))
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn batch_inscribe_with_satpoints_with_parent() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let parent_output =
    CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png")
      .write("parent.png", [1; 520])
      .core(&core)
      .ord(&ord)
      .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  let txids = core
    .mine_blocks(3)
    .iter()
    .map(|block| block.txdata[0].compute_txid())
    .collect::<Vec<Txid>>();

  let satpoint_1 = SatPoint {
    outpoint: OutPoint {
      txid: txids[0],
      vout: 0,
    },
    offset: 0,
  };

  let satpoint_2 = SatPoint {
    outpoint: OutPoint {
      txid: txids[1],
      vout: 0,
    },
    offset: 0,
  };

  let satpoint_3 = SatPoint {
    outpoint: OutPoint {
      txid: txids[2],
      vout: 0,
    },
    offset: 0,
  };

  let sat_1 = serde_json::from_str::<api::Output>(
    &ord
      .json_request(format!("/output/{}", satpoint_1.outpoint))
      .text()
      .unwrap(),
  )
  .unwrap()
  .sat_ranges
  .unwrap()[0]
    .0;

  let sat_2 = serde_json::from_str::<api::Output>(
    &ord
      .json_request(format!("/output/{}", satpoint_2.outpoint))
      .text()
      .unwrap(),
  )
  .unwrap()
  .sat_ranges
  .unwrap()[0]
    .0;

  let sat_3 = serde_json::from_str::<api::Output>(
    &ord
      .json_request(format!("/output/{}", satpoint_3.outpoint))
      .text()
      .unwrap(),
  )
  .unwrap()
  .sat_ranges
  .unwrap()[0]
    .0;

  let parent_id = parent_output.inscriptions[0].id;

  let output = CommandBuilder::new("--index-sats wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 555])
    .write("meow.wav", [0; 2048])
    .write(
      "batch.yaml",
      format!(
        r#"
mode: satpoints
parents:
- {parent_id}
inscriptions:
- file: inscription.txt
  satpoint: {satpoint_1}
- file: tulip.png
  satpoint: {satpoint_2}
- file: meow.wav
  satpoint: {satpoint_3}
"#
      ),
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  ord.assert_response_regex(
    format!("/inscription/{parent_id}"),
    format!(r".*<dt>location</dt>.*{}:0:0.*", output.reveal),
  );

  for inscription in &output.inscriptions {
    assert_eq!(inscription.location.offset, 0);
  }

  let outpoints = output
    .inscriptions
    .iter()
    .map(|inscription| inscription.location.outpoint)
    .collect::<Vec<OutPoint>>();

  assert_eq!(outpoints.len(), output.inscriptions.len());

  let inscription_1 = &output.inscriptions[0];
  let inscription_2 = &output.inscriptions[1];
  let inscription_3 = &output.inscriptions[2];

  ord.assert_response_regex(
    format!("/inscription/{}", inscription_1.id),
    format!(r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>{}</dd>.*<dt>sat</dt>.*<dd>.*{}.*</dd>.*<dt>location</dt>.*{}</a></dd>.*",
      50 * COIN_VALUE,
      sat_1,
      inscription_1.location,
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", inscription_2.id),
    format!(r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>{}</dd>.*<dt>sat</dt>.*<dd>.*{}.*</dd>.*<dt>location</dt>.*{}</a></dd>.*",
       50 * COIN_VALUE,
       sat_2,
       inscription_2.location
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", inscription_3.id),
    format!(r".*<dt>parents</dt>\s*<dd>.*{parent_id}.*</dd>.*<dt>value</dt>.*<dd>{}</dd>.*<dt>sat</dt>.*<dd>.*{}.*</dd>.*<dt>location</dt>.*{}</a></dd>.*",
      50 * COIN_VALUE,
      sat_3,
      inscription_3.location
    ),
  );
}

#[test]
fn batch_inscribe_with_satpoints_with_different_sizes() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]);

  create_wallet(&core, &ord);

  let address_1 = receive(&core, &ord);
  let address_2 = receive(&core, &ord);
  let address_3 = receive(&core, &ord);

  core.mine_blocks(3);

  let outpoint_1 = OutPoint {
    txid: CommandBuilder::new(format!(
      "--index-sats wallet send --fee-rate 1 {address_1} 25btc"
    ))
    .core(&core)
    .ord(&ord)
    .stdout_regex(r".*")
    .run_and_deserialize_output::<send::Output>()
    .txid,
    vout: 0,
  };

  core.mine_blocks(1);

  let outpoint_2 = OutPoint {
    txid: CommandBuilder::new(format!(
      "--index-sats wallet send --fee-rate 1 {address_2} 1btc"
    ))
    .core(&core)
    .ord(&ord)
    .stdout_regex(r".*")
    .run_and_deserialize_output::<send::Output>()
    .txid,
    vout: 0,
  };

  core.mine_blocks(1);

  let outpoint_3 = OutPoint {
    txid: CommandBuilder::new(format!(
      "--index-sats wallet send --fee-rate 1 {address_3} 3btc"
    ))
    .core(&core)
    .ord(&ord)
    .stdout_regex(r".*")
    .run_and_deserialize_output::<send::Output>()
    .txid,
    vout: 0,
  };

  core.mine_blocks(1);

  let satpoint_1 = SatPoint {
    outpoint: outpoint_1,
    offset: 0,
  };

  let satpoint_2 = SatPoint {
    outpoint: outpoint_2,
    offset: 0,
  };

  let satpoint_3 = SatPoint {
    outpoint: outpoint_3,
    offset: 0,
  };

  let output_1 = serde_json::from_str::<api::Output>(
    &ord
      .json_request(format!("/output/{}", satpoint_1.outpoint))
      .text()
      .unwrap(),
  )
  .unwrap();
  assert_eq!(output_1.value, 25 * COIN_VALUE);

  let output_2 = serde_json::from_str::<api::Output>(
    &ord
      .json_request(format!("/output/{}", satpoint_2.outpoint))
      .text()
      .unwrap(),
  )
  .unwrap();
  assert_eq!(output_2.value, COIN_VALUE);

  let output_3 = serde_json::from_str::<api::Output>(
    &ord
      .json_request(format!("/output/{}", satpoint_3.outpoint))
      .text()
      .unwrap(),
  )
  .unwrap();
  assert_eq!(output_3.value, 3 * COIN_VALUE);

  let sat_1 = output_1.sat_ranges.unwrap()[0].0;
  let sat_2 = output_2.sat_ranges.unwrap()[0].0;
  let sat_3 = output_3.sat_ranges.unwrap()[0].0;

  let output = CommandBuilder::new("--index-sats wallet batch --fee-rate 1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write("tulip.png", [0; 5])
    .write("meow.wav", [0; 2])
    .write(
      "batch.yaml",
      format!(
        r#"
mode: satpoints
inscriptions:
- file: inscription.txt
  satpoint: {satpoint_1}
- file: tulip.png
  satpoint: {satpoint_2}
- file: meow.wav
  satpoint: {satpoint_3}
"#
      ),
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  for inscription in &output.inscriptions {
    assert_eq!(inscription.location.offset, 0);
  }

  let outpoints = output
    .inscriptions
    .iter()
    .map(|inscription| inscription.location.outpoint)
    .collect::<Vec<OutPoint>>();

  assert_eq!(outpoints.len(), output.inscriptions.len());

  let inscription_1 = &output.inscriptions[0];
  let inscription_2 = &output.inscriptions[1];
  let inscription_3 = &output.inscriptions[2];

  ord.assert_response_regex(
    format!("/inscription/{}", inscription_1.id),
    format!(
      r".*<dt>value</dt>.*<dd>{}</dd>.*<dt>sat</dt>.*<dd>.*{}.*</dd>.*<dt>location</dt>.*{}.*",
      25 * COIN_VALUE,
      sat_1,
      inscription_1.location
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", inscription_2.id),
    format!(
      r".*<dt>value</dt>.*<dd>{}</dd>.*<dt>sat</dt>.*<dd>.*{}.*</dd>.*<dt>location</dt>.*{}.*",
      COIN_VALUE, sat_2, inscription_2.location
    ),
  );

  ord.assert_response_regex(
    format!("/inscription/{}", inscription_3.id),
    format!(
      r".*<dt>value</dt>.*<dd>{}</dd>.*<dt>sat</dt>.*<dd>.*{}.*</dd>.*<dt>location</dt>.*{}.*",
      3 * COIN_VALUE,
      sat_3,
      inscription_3.location
    ),
  );
}

#[test]
fn batch_can_etch_rune() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let rune = SpacedRune {
    rune: Rune(RUNE),
    spacers: 0,
  };

  let batch = batch(
    &core,
    &ord,
    batch::File {
      etching: Some(batch::Etching {
        divisibility: 0,
        rune,
        supply: "1000".parse().unwrap(),
        premine: "1000".parse().unwrap(),
        symbol: '¢',
        terms: None,
        turbo: false,
      }),
      inscriptions: vec![batch::Entry {
        file: Some("inscription.jpeg".into()),
        ..default()
      }],
      ..default()
    },
  );

  let parent = batch.output.inscriptions[0].id;

  let request = ord.request(format!("/content/{parent}"));

  assert_eq!(request.status(), 200);
  assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg");
  assert_eq!(request.text().unwrap(), "inscription");

  ord.assert_response_regex(
    format!("/inscription/{parent}"),
    r".*<dt>rune</dt>\s*<dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>.*",
  );

  ord.assert_response_regex(
    "/rune/AAAAAAAAAAAAA",
    format!(
      r".*\s*<dt>turbo</dt>\s*<dd>false</dd>.*<dt>parent</dt>\s*<dd><a class=collapse href=/inscription/{parent}>{parent}</a></dd>.*"
    ),
  );

  let destination = batch
    .output
    .rune
    .unwrap()
    .destination
    .unwrap()
    .require_network(Network::Regtest)
    .unwrap();

  assert!(core.state().is_wallet_address(&destination));

  let reveal = core.tx_by_id(batch.output.reveal);

  assert_eq!(
    reveal.input[0].sequence,
    Sequence::from_height(Runestone::COMMIT_CONFIRMATIONS - 1)
  );

  let Artifact::Runestone(runestone) = Runestone::decipher(&reveal).unwrap() else {
    panic!();
  };

  let pointer = reveal.output.len() - 2;

  assert_eq!(runestone.pointer, Some(pointer.try_into().unwrap()));

  assert_eq!(
    reveal.output[pointer].script_pubkey,
    destination.script_pubkey(),
  );

  assert_eq!(
    CommandBuilder::new("--regtest wallet balance")
      .core(&core)
      .ord(&ord)
      .run_and_deserialize_output::<Balance>(),
    Balance {
      cardinal: 39999980000,
      ordinal: 10000,
      runic: Some(10000),
      runes: Some(
        vec![(
          rune,
          Decimal {
            value: 1000,
            scale: 0,
          }
        )]
        .into_iter()
        .collect()
      ),
      total: 400 * COIN_VALUE,
    }
  );
}

#[test]
fn batch_can_etch_turbo_rune() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let rune = SpacedRune {
    rune: Rune(RUNE),
    spacers: 0,
  };

  let batch = batch(
    &core,
    &ord,
    batch::File {
      etching: Some(batch::Etching {
        divisibility: 0,
        rune,
        supply: "1000".parse().unwrap(),
        premine: "1000".parse().unwrap(),
        symbol: '¢',
        terms: None,
        turbo: true,
      }),
      inscriptions: vec![batch::Entry {
        file: Some("inscription.jpeg".into()),
        ..default()
      }],
      ..default()
    },
  );

  let parent = batch.output.inscriptions[0].id;

  ord.assert_response_regex(
    "/rune/AAAAAAAAAAAAA",
    format!(
      r".*\s*<dt>turbo</dt>\s*<dd>true</dd>.*<dt>parent</dt>\s*<dd><a class=collapse href=/inscription/{parent}>{parent}</a></dd>.*"
    ),
  );
}

#[test]
fn batch_can_etch_rune_without_premine() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let rune = SpacedRune {
    rune: Rune(RUNE),
    spacers: 0,
  };

  let batch = batch(
    &core,
    &ord,
    batch::File {
      etching: Some(batch::Etching {
        divisibility: 0,
        rune,
        supply: "1000".parse().unwrap(),
        premine: "0".parse().unwrap(),
        symbol: '¢',
        terms: Some(batch::Terms {
          cap: 1,
          amount: "1000".parse().unwrap(),
          height: None,
          offset: None,
        }),
        turbo: false,
      }),
      inscriptions: vec![batch::Entry {
        file: Some("inscription.jpeg".into()),
        ..default()
      }],
      ..default()
    },
  );

  let parent = batch.output.inscriptions[0].id;

  let request = ord.request(format!("/content/{parent}"));

  assert_eq!(request.status(), 200);
  assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg");
  assert_eq!(request.text().unwrap(), "inscription");

  ord.assert_response_regex(
    format!("/inscription/{parent}"),
    r".*<dt>rune</dt>\s*<dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>.*",
  );

  ord.assert_response_regex(
    "/rune/AAAAAAAAAAAAA",
    format!(
      r".*<dt>parent</dt>\s*<dd><a class=collapse href=/inscription/{parent}>{parent}</a></dd>.*"
    ),
  );

  assert_eq!(batch.output.rune.unwrap().destination, None);

  let reveal = core.tx_by_id(batch.output.reveal);

  assert_eq!(
    reveal.input[0].sequence,
    Sequence::from_height(Runestone::COMMIT_CONFIRMATIONS - 1)
  );

  assert_eq!(
    CommandBuilder::new("--regtest wallet balance")
      .core(&core)
      .ord(&ord)
      .run_and_deserialize_output::<Balance>(),
    Balance {
      cardinal: 39999990000,
      ordinal: 10000,
      runic: Some(0),
      runes: Some(default()),
      total: 400 * COIN_VALUE,
    }
  );
}

#[test]
fn batch_inscribe_can_etch_rune_with_offset() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let batch = batch(
    &core,
    &ord,
    batch::File {
      etching: Some(batch::Etching {
        divisibility: 0,
        rune: SpacedRune {
          rune: Rune(RUNE),
          spacers: 0,
        },
        supply: "10000".parse().unwrap(),
        premine: "1000".parse().unwrap(),
        symbol: '¢',
        terms: Some(batch::Terms {
          cap: 9,
          amount: "1000".parse().unwrap(),
          offset: Some(batch::Range {
            start: Some(10),
            end: Some(20),
          }),
          height: None,
        }),
        turbo: false,
      }),
      inscriptions: vec![batch::Entry {
        file: Some("inscription.jpeg".into()),
        ..default()
      }],
      ..default()
    },
  );

  let parent = batch.output.inscriptions[0].id;

  let request = ord.request(format!("/content/{parent}"));

  assert_eq!(request.status(), 200);
  assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg");
  assert_eq!(request.text().unwrap(), "inscription");

  ord.assert_response_regex(
    format!("/inscription/{parent}"),
    r".*<dt>rune</dt>\s*<dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>.*",
  );

  ord.assert_response_regex(
    "/rune/AAAAAAAAAAAAA",
    format!(
      r".*<dt>parent</dt>\s*<dd><a class=collapse href=/inscription/{parent}>{parent}</a></dd>.*"
    ),
  );

  assert!(
    core.state().is_wallet_address(
      &batch
        .output
        .rune
        .unwrap()
        .destination
        .unwrap()
        .require_network(Network::Regtest)
        .unwrap()
    )
  );
}

#[test]
fn batch_inscribe_can_etch_rune_with_height() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let batch = batch(
    &core,
    &ord,
    batch::File {
      etching: Some(batch::Etching {
        divisibility: 0,
        rune: SpacedRune {
          rune: Rune(RUNE),
          spacers: 0,
        },
        supply: "10000".parse().unwrap(),
        premine: "1000".parse().unwrap(),
        symbol: '¢',
        terms: Some(batch::Terms {
          cap: 9,
          amount: "1000".parse().unwrap(),
          height: Some(batch::Range {
            start: Some(10),
            end: Some(20),
          }),
          offset: None,
        }),
        turbo: false,
      }),
      inscriptions: vec![batch::Entry {
        file: Some("inscription.jpeg".into()),
        ..default()
      }],
      ..default()
    },
  );

  let parent = batch.output.inscriptions[0].id;

  let request = ord.request(format!("/content/{parent}"));

  assert_eq!(request.status(), 200);
  assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg");
  assert_eq!(request.text().unwrap(), "inscription");

  ord.assert_response_regex(
    format!("/inscription/{parent}"),
    r".*<dt>rune</dt>\s*<dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>.*",
  );

  ord.assert_response_regex(
    "/rune/AAAAAAAAAAAAA",
    format!(
      r".*<dt>parent</dt>\s*<dd><a class=collapse href=/inscription/{parent}>{parent}</a></dd>.*"
    ),
  );

  assert!(
    core.state().is_wallet_address(
      &batch
        .output
        .rune
        .unwrap()
        .destination
        .unwrap()
        .require_network(Network::Regtest)
        .unwrap()
    )
  );
}

#[test]
fn etch_existing_rune_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  etch(&core, &ord, Rune(RUNE));

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 1,
          },
          supply: "1000".parse().unwrap(),
          premine: "1000".parse().unwrap(),
          symbol: '¢',
          terms: None,
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: rune `AAAAAAAAAAAAA` has already been etched\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn etch_reserved_rune_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune::reserved(0, 0),
            spacers: 0,
          },
          premine: "1000".parse().unwrap(),
          supply: "1000".parse().unwrap(),
          symbol: '¢',
          terms: None,
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: rune `AAAAAAAAAAAAAAAAAAAAAAAAAAA` is reserved\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn etch_sub_minimum_rune_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
          supply: "1000".parse().unwrap(),
          premine: "1000".parse().unwrap(),
          symbol: '¢',
          terms: None,
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: rune is less than minimum for next block: A < ZZRZCNJJBILX\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn etch_requires_rune_index() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "1000".parse().unwrap(),
          premine: "1000".parse().unwrap(),
          symbol: '¢',
          terms: None,
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: etching runes requires index created with `--index-runes`\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn etch_divisibility_over_maximum_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 39,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "1000".parse().unwrap(),
          premine: "1000".parse().unwrap(),
          symbol: '¢',
          terms: None,
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: <DIVISIBILITY> must be less than or equal 38\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn etch_mintable_overflow_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: default(),
          premine: default(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 2,
            offset: Some(batch::Range {
              end: Some(2),
              start: None,
            }),
            amount: "340282366920938463463374607431768211455".parse().unwrap(),
            height: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: `terms.cap` * `terms.amount` over maximum\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn etch_mintable_plus_premine_overflow_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: default(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 1,
            offset: Some(batch::Range {
              end: Some(2),
              start: None,
            }),
            amount: "340282366920938463463374607431768211455".parse().unwrap(),
            height: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: `premine` + `terms.cap` * `terms.amount` over maximum\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn incorrect_supply_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "1".parse().unwrap(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 1,
            offset: Some(batch::Range {
              end: Some(2),
              start: None,
            }),
            amount: "1".parse().unwrap(),
            height: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: `supply` not equal to `premine` + `terms.cap` * `terms.amount`\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn zero_offset_interval_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "2".parse().unwrap(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 1,
            offset: Some(batch::Range {
              end: Some(2),
              start: Some(2),
            }),
            amount: "1".parse().unwrap(),
            height: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: `terms.offset.end` must be greater than `terms.offset.start`\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn zero_height_interval_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "2".parse().unwrap(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 1,
            height: Some(batch::Range {
              end: Some(2),
              start: Some(2),
            }),
            amount: "1".parse().unwrap(),
            offset: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: `terms.height.end` must be greater than `terms.height.start`\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn invalid_start_height_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "2".parse().unwrap(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 1,
            height: Some(batch::Range {
              end: None,
              start: Some(0),
            }),
            amount: "1".parse().unwrap(),
            offset: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr(
      "error: `terms.height.start` must be greater than the reveal transaction block height of 7\n",
    )
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn invalid_end_height_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "2".parse().unwrap(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 1,
            height: Some(batch::Range {
              start: None,
              end: Some(0),
            }),
            amount: "1".parse().unwrap(),
            offset: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr(
      "error: `terms.height.end` must be greater than the reveal transaction block height of 7\n",
    )
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn zero_supply_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "0".parse().unwrap(),
          premine: "0".parse().unwrap(),
          symbol: '¢',
          terms: None,
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: `supply` must be greater than zero\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn zero_cap_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "1".parse().unwrap(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 0,
            height: None,
            amount: "1".parse().unwrap(),
            offset: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: `terms.cap` must be greater than zero\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn zero_amount_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "1".parse().unwrap(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: Some(batch::Terms {
            cap: 1,
            height: None,
            amount: "0".parse().unwrap(),
            offset: None,
          }),
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: `terms.amount` must be greater than zero\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn oversize_runestone_error() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(6402364363415443603228541259936211926 - 1),
            spacers: 0b00000111_11111111_11111111_11111111,
          },
          supply: u128::MAX.to_string().parse().unwrap(),
          premine: (u128::MAX - 1).to_string().parse().unwrap(),
          symbol: '\u{10FFFF}',
          terms: Some(batch::Terms {
            cap: 1,
            height: Some(batch::Range {
              start: Some(u64::MAX - 1),
              end: Some(u64::MAX),
            }),
            offset: Some(batch::Range {
              start: Some(u64::MAX - 1),
              end: Some(u64::MAX),
            }),
            amount: "1".parse().unwrap(),
          }),
          turbo: true,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: runestone greater than maximum OP_RETURN size: 104 > 83\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn oversize_runestones_are_allowed_with_no_limit() {
  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new(
    "--regtest --index-runes wallet batch --fee-rate 0 --dry-run --no-limit --batch batch.yaml",
  )
  .write("inscription.txt", "foo")
  .write(
    "batch.yaml",
    serde_yaml::to_string(&batch::File {
      etching: Some(batch::Etching {
        divisibility: 0,
        rune: SpacedRune {
          rune: Rune(6402364363415443603228541259936211926 - 1),
          spacers: 0b00000111_11111111_11111111_11111111,
        },
        supply: u128::MAX.to_string().parse().unwrap(),
        premine: (u128::MAX - 1).to_string().parse().unwrap(),
        symbol: '\u{10FFFF}',
        terms: Some(batch::Terms {
          cap: 1,
          height: Some(batch::Range {
            start: Some(u64::MAX - 1),
            end: Some(u64::MAX),
          }),
          offset: Some(batch::Range {
            start: Some(u64::MAX - 1),
            end: Some(u64::MAX),
          }),
          amount: "1".parse().unwrap(),
        }),
        turbo: true,
      }),
      inscriptions: vec![batch::Entry {
        file: Some("inscription.txt".into()),
        ..default()
      }],
      ..default()
    })
    .unwrap(),
  )
  .core(&core)
  .ord(&ord)
  .run_and_deserialize_output::<Batch>();
}

#[cfg(unix)]
#[test]
fn batch_inscribe_errors_if_pending_etchings() {
  use nix::{
    sys::signal::{self, Signal},
    unistd::Pid,
  };

  let core = mockcore::builder().network(Network::Regtest).build();

  let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  let batchfile = batch::File {
    etching: Some(batch::Etching {
      divisibility: 0,
      rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      supply: "1000".parse().unwrap(),
      premine: "1000".parse().unwrap(),
      symbol: '¢',
      ..default()
    }),
    inscriptions: vec![batch::Entry {
      file: Some("inscription.jpeg".into()),
      ..default()
    }],
    ..default()
  };

  let tempdir = Arc::new(TempDir::new().unwrap());

  {
    let mut spawn =
      CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
        .temp_dir(tempdir.clone())
        .write("batch.yaml", serde_yaml::to_string(&batchfile).unwrap())
        .write("inscription.jpeg", "inscription")
        .core(&core)
        .ord(&ord)
        .expected_exit_code(1)
        .spawn();

    let mut buffer = String::new();

    BufReader::new(spawn.child.stderr.as_mut().unwrap())
      .read_line(&mut buffer)
      .unwrap();

    assert_regex_match!(
      buffer,
      "Waiting for rune .* commitment [[:xdigit:]]{64} to mature…\n"
    );

    core.mine_blocks(1);

    signal::kill(
      Pid::from_raw(spawn.child.id().try_into().unwrap()),
      Signal::SIGINT,
    )
    .unwrap();

    buffer.clear();

    BufReader::new(spawn.child.stderr.as_mut().unwrap())
      .read_line(&mut buffer)
      .unwrap();

    assert_eq!(
      buffer,
      "Shutting down gracefully. Press <CTRL-C> again to shutdown immediately.\n"
    );

    spawn.child.wait().unwrap();
  }

  CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .temp_dir(tempdir)
    .core(&core)
    .ord(&ord)
    .expected_exit_code(1)
    .expected_stderr(
      "error: rune `AAAAAAAAAAAAA` has pending etching, resume with `ord wallet resume`\n",
    )
    .run_and_extract_stdout();
}

#[test]
fn forbid_etching_below_rune_activation_height() {
  let core = mockcore::builder().build();

  let ord = TestServer::spawn_with_server_args(&core, &["--index-runes"], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("--index-runes wallet batch --fee-rate 0 --batch batch.yaml")
    .write("inscription.txt", "foo")
    .write(
      "batch.yaml",
      serde_yaml::to_string(&batch::File {
        etching: Some(batch::Etching {
          divisibility: 0,
          rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          supply: "1".parse().unwrap(),
          premine: "1".parse().unwrap(),
          symbol: '¢',
          terms: None,
          turbo: false,
        }),
        inscriptions: vec![batch::Entry {
          file: Some("inscription.txt".into()),
          ..default()
        }],
        ..default()
      })
      .unwrap(),
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr("error: rune reveal height below rune activation height: 7 < 840000\n")
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn batch_inscribe_can_create_inscription_with_gallery() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  let (id0, _) = inscribe(&core, &ord);
  let (id1, _) = inscribe(&core, &ord);

  core.mine_blocks(1);

  let output = CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write(
      "batch.yaml",
      format!(
        "
mode: shared-output
inscriptions:
- file: inscription.txt
  gallery:
  - id: {id0}
    title: foo
    traits:
      foo: true
  - id: {id1}
    title: bar
    traits:
      bar: false
"
      ),
    )
    .core(&core)
    .ord(&ord)
    .run_and_deserialize_output::<Batch>();

  let gallery = output.inscriptions[0].id;

  core.mine_blocks(1);

  let request = ord.request(format!("/content/{gallery}"));

  assert_eq!(request.status(), 200);
  assert_eq!(
    request.headers().get("content-type").unwrap(),
    "text/plain;charset=utf-8"
  );
  assert_eq!(request.text().unwrap(), "Hello World");

  ord.assert_response_regex(
    format!("/inscription/{gallery}"),
    format!(
      r".*
  <dt>gallery</dt>
  <dd>
    <div class=thumbnails>
      <a href=/gallery/{gallery}/0>.*<iframe .* src=/preview/{id0}></iframe></a>
      <a href=/gallery/{gallery}/1>.*<iframe .* src=/preview/{id1}></iframe></a>
    </div>
    <div class=center>
      <a href=/gallery/{gallery}>all \(2\)</a>
    </div>
  </dd>
.*"
    ),
  );

  ord.assert_html(
    format!("/gallery/{gallery}/0"),
    Chain::Mainnet,
    ItemHtml {
      gallery_id: gallery,
      gallery_number: -1,
      i: 0,
      item: Item {
        id: Some(id0),
        attributes: Attributes {
          title: Some("foo".into()),
          traits: Traits {
            items: vec![("foo".into(), Trait::Bool(true))],
          },
        },
        index: None,
      },
    },
  );

  ord.assert_html(
    format!("/gallery/{gallery}/1"),
    Chain::Mainnet,
    ItemHtml {
      gallery_id: gallery,
      gallery_number: -1,
      i: 1,
      item: Item {
        id: Some(id1),
        attributes: Attributes {
          title: Some("bar".into()),
          traits: Traits {
            items: vec![("bar".into(), Trait::Bool(false))],
          },
        },
        index: None,
      },
    },
  );

  let request = ord.request(format!("/gallery/{gallery}/2"));
  assert_eq!(request.status(), 404);
  assert_eq!(
    request.text().unwrap(),
    format!("gallery {gallery} item 2 not found"),
  );

  let request = ord.request("/gallery/100/2");
  assert_eq!(request.status(), 404);
  assert_eq!(
    request.text().unwrap(),
    format!("inscription 100 not found"),
  );

  let request = ord.request("/gallery/hello/2");
  assert_eq!(request.status(), 404);
  assert_eq!(request.text().unwrap(), format!("sat index required"));
}

#[test]
fn batch_inscribe_fails_if_gallery_inscription_does_not_exist() {
  let core = mockcore::spawn();

  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);

  core.mine_blocks(1);

  CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml")
    .write("inscription.txt", "Hello World")
    .write(
      "batch.yaml",
      "
mode: shared-output
inscriptions:
- file: inscription.txt
  gallery:
  - id: 0000000000000000000000000000000000000000000000000000000000000000i0
",
    )
    .core(&core)
    .ord(&ord)
    .expected_stderr(
      "error: inscriptions referenced in batchfile do not exist: \
      0000000000000000000000000000000000000000000000000000000000000000i0\n",
    )
    .expected_exit_code(1)
    .run_and_extract_stdout();
}

#[test]
fn batch_can_compress() {
  let core = mockcore::spawn();
  let ord = TestServer::spawn_with_server_args(&core, &[], &[]);

  create_wallet(&core, &ord);
  core.mine_blocks(1);

  let title = "a]".repeat(100);

  let Batch { reveal, .. } =
    CommandBuilder::new("wallet batch --compress --fee-rate 1 --batch batch.yaml")
      .write(
        "batch.yaml",
        format!("mode: shared-output\ninscriptions:\n- file: foo.txt\n  title: {title}\n"),
      )
      .write("foo.txt", [0; 350_000])
      .core(&core)
      .ord(&ord)
      .run_and_deserialize_output();

  core.mine_blocks(1);

  let response = ord.json_request(format!("/decode/{reveal}"));
  assert_eq!(response.status(), StatusCode::OK);

  let decode: api::Decode = serde_json::from_str(&response.text().unwrap()).unwrap();
  let inscription = &decode.inscriptions[0].payload;

  assert_eq!(inscription.content_encoding, Some(b"br".to_vec()));
  assert_eq!(inscription.property_encoding, Some(b"br".to_vec()));

  let compressed_body = inscription.body.as_ref().unwrap();
  let mut decompressor =
    brotli::Decompressor::new(compressed_body.as_slice(), compressed_body.len());
  let mut decompressed_body = Vec::new();
  decompressor.read_to_end(&mut decompressed_body).unwrap();
  assert_eq!(decompressed_body, vec![0; 350_000]);

  let compressed_properties = inscription.properties.as_ref().unwrap();
  let mut decompressor = brotli::Decompressor::new(
    compressed_properties.as_slice(),
    compressed_properties.len(),
  );
  let mut decompressed_properties = Vec::new();
  decompressor
    .read_to_end(&mut decompressed_properties)
    .unwrap();

  let properties = minicbor::decode::<Properties>(&decompressed_properties).unwrap();
  assert_eq!(properties.attributes.title, Some(title));
}
