use {
  self::{
    accept_encoding::AcceptEncoding,
    accept_json::AcceptJson,
    error::{OptionExt, ServerError, ServerResult},
  },
  super::*,
  crate::templates::{
    AddressHtml, BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, GalleriesHtml,
    GalleryHtml, HomeHtml, InputHtml, InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml,
    ItemHtml, OutputHtml, PageContent, PageHtml, ParentsHtml, PreviewAudioHtml, PreviewCodeHtml,
    PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml,
    PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RareTxt, RuneHtml, RuneNotFoundHtml,
    RunesHtml, SatHtml, SatscardHtml, TransactionHtml,
  },
  axum::{
    Router,
    extract::{DefaultBodyLimit, Extension, Json, Path, Query},
    http::{self, HeaderMap, HeaderName, HeaderValue, StatusCode, Uri, header},
    response::{IntoResponse, Redirect, Response},
    routing::{get, post},
  },
  axum_server::Handle,
  rust_embed::RustEmbed,
  rustls_acme::{
    AcmeConfig,
    acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY},
    axum::AxumAcceptor,
    caches::DirCache,
  },
  tokio_stream::StreamExt,
  tower_http::{
    compression::CompressionLayer,
    cors::{Any, CorsLayer},
    set_header::SetResponseHeaderLayer,
    validate_request::ValidateRequestHeaderLayer,
  },
};

pub use server_config::ServerConfig;

mod accept_encoding;
mod accept_json;
mod error;
pub mod query;
mod r;
mod server_config;

const MEBIBYTE: usize = 1 << 20;
const PAGE_SIZE: usize = 100;

enum SpawnConfig {
  Https(AxumAcceptor),
  Http,
  Redirect(String),
}

#[derive(Deserialize)]
pub(crate) struct OutputsQuery {
  #[serde(rename = "type")]
  pub(crate) ty: Option<OutputType>,
}

#[derive(Clone, Copy, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub(crate) enum OutputType {
  #[default]
  Any,
  Cardinal,
  Inscribed,
  Runic,
}

#[derive(Deserialize)]
struct Search {
  query: String,
}

#[derive(RustEmbed)]
#[folder = "static"]
struct StaticAssets;

static SAT_AT_INDEX_PATH: LazyLock<Regex> =
  LazyLock::new(|| Regex::new(r"^/r/sat/[^/]+/at/[^/]+$").unwrap());

#[derive(Debug, Parser, Clone)]
pub struct Server {
  #[arg(long, help = "Accept PSBT offer submissions to server.")]
  pub(crate) accept_offers: bool,
  #[arg(
    long,
    help = "Listen on <ADDRESS> for incoming requests. [default: 0.0.0.0]"
  )]
  pub(crate) address: Option<String>,
  #[arg(
    long,
    help = "Request ACME TLS certificate for <ACME_DOMAIN>. This ord instance must be reachable at <ACME_DOMAIN>:443 to respond to Let's Encrypt ACME challenges."
  )]
  pub(crate) acme_domain: Vec<String>,
  #[arg(
    long,
    help = "Use <CSP_ORIGIN> in Content-Security-Policy header. Set this to the public-facing URL of your ord instance."
  )]
  pub(crate) csp_origin: Option<String>,
  #[arg(
    long,
    help = "Decompress encoded content. Currently only supports brotli. Be careful using this on production instances. A decompressed inscription may be arbitrarily large, making decompression a DoS vector."
  )]
  pub(crate) decompress: bool,
  #[arg(long, env = "ORD_SERVER_DISABLE_JSON_API", help = "Disable JSON API.")]
  pub(crate) disable_json_api: bool,
  #[arg(
    long,
    help = "Listen on <HTTP_PORT> for incoming HTTP requests. [default: 80]"
  )]
  pub(crate) http_port: Option<u16>,
  #[arg(
    long,
    group = "port",
    help = "Listen on <HTTPS_PORT> for incoming HTTPS requests. [default: 443]"
  )]
  pub(crate) https_port: Option<u16>,
  #[arg(long, help = "Store ACME TLS certificates in <ACME_CACHE>.")]
  pub(crate) acme_cache: Option<PathBuf>,
  #[arg(long, help = "Provide ACME contact <ACME_CONTACT>.")]
  pub(crate) acme_contact: Vec<String>,
  #[arg(long, help = "Serve HTTP traffic on <HTTP_PORT>.")]
  pub(crate) http: bool,
  #[arg(long, help = "Serve HTTPS traffic on <HTTPS_PORT>.")]
  pub(crate) https: bool,
  #[arg(long, help = "Redirect HTTP traffic to HTTPS.")]
  pub(crate) redirect_http_to_https: bool,
  #[arg(long, alias = "nosync", help = "Do not update the index.")]
  pub(crate) no_sync: bool,
  #[arg(
    long,
    help = "Proxy `/content/INSCRIPTION_ID` and other recursive endpoints to `<PROXY>` if the inscription is not present on current chain."
  )]
  pub(crate) proxy: Option<Url>,
  #[arg(
    long,
    default_value = "5s",
    help = "Poll Bitcoin Core every <POLLING_INTERVAL>."
  )]
  pub(crate) polling_interval: humantime::Duration,
}

impl Server {
  pub fn run(
    self,
    settings: Settings,
    index: Arc<Index>,
    handle: Handle<SocketAddr>,
    http_port_tx: Option<std::sync::mpsc::Sender<u16>>,
  ) -> SubcommandResult {
    settings.runtime()?.block_on(async {
      let index_clone = index.clone();
      let integration_test = settings.integration_test();

      if (cfg!(test) || integration_test) && !self.no_sync {
        index.update().unwrap();
      }

      let index_thread = thread::spawn(move || {
        loop {
          if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
            break;
          }

          if !self.no_sync
            && let Err(error) = index_clone.update()
          {
            log::warn!("Updating index: {error}");
          }

          thread::sleep(if integration_test {
            Duration::from_millis(100)
          } else {
            self.polling_interval.into()
          });
        }
      });

      INDEXER.lock().unwrap().replace(index_thread);

      let settings = Arc::new(settings);
      let acme_domains = self.acme_domains()?;

      let server_config = Arc::new(ServerConfig {
        accept_offers: self.accept_offers,
        chain: settings.chain(),
        csp_origin: self.csp_origin.clone(),
        decompress: self.decompress,
        domain: acme_domains.first().cloned(),
        index_sats: index.has_sat_index(),
        json_api_enabled: !self.disable_json_api,
        proxy: self.proxy.clone(),
      });

      let body_limit = if server_config.json_api_enabled {
        DefaultBodyLimit::max(32 * MEBIBYTE)
      } else {
        DefaultBodyLimit::max(2 * MEBIBYTE)
      };

      // non-recursive endpoints
      let router = Router::new()
        .route("/", get(Self::home))
        .route("/address/{address}", get(Self::address))
        .route("/block/{query}", get(Self::block))
        .route("/blockcount", get(Self::block_count))
        .route("/blocks", get(Self::blocks))
        .route("/bounties", get(Self::bounties))
        .route("/children/{inscription_id}", get(Self::children))
        .route(
          "/children/{inscription_id}/{page}",
          get(Self::children_paginated),
        )
        .route("/clock", get(Self::clock))
        .route("/collections", get(Self::collections))
        .route("/collections/{page}", get(Self::collections_paginated))
        .route("/decode/{txid}", get(Self::decode))
        .route("/galleries", get(Self::galleries))
        .route("/galleries/{page}", get(Self::galleries_paginated))
        .route("/faq", get(Self::faq))
        .route("/favicon.ico", get(Self::favicon))
        .route("/feed.xml", get(Self::feed))
        .route("/gallery/{inscription_id}", get(Self::gallery))
        .route(
          "/gallery/{inscription_id}/page/{page}",
          get(Self::gallery_paginated),
        )
        .route("/gallery/{inscription_query}/{item}", get(Self::item))
        .route("/input/{block}/{transaction}/{input}", get(Self::input))
        .route("/inscription/{inscription_query}", get(Self::inscription))
        .route(
          "/inscription/{inscription_query}/{child}",
          get(Self::inscription_child),
        )
        .route("/inscriptions", get(Self::inscriptions))
        .route(
          "/inscriptions",
          post(Self::inscriptions_json).layer(body_limit),
        )
        .route(
          "/inscriptions/block/{height}",
          get(Self::inscriptions_in_block),
        )
        .route(
          "/inscriptions/block/{height}/{page}",
          get(Self::inscriptions_in_block_paginated),
        )
        .route("/inscriptions/{page}", get(Self::inscriptions_paginated))
        .route("/install.sh", get(Self::install_script))
        .route("/missing", post(Self::missing).layer(body_limit))
        .route("/offer", post(Self::offer))
        .route("/offers", get(Self::offers))
        .route("/ordinal/{sat}", get(Self::ordinal))
        .route("/output/{output}", get(Self::output))
        .route("/outputs", post(Self::outputs).layer(body_limit))
        .route("/outputs/{address}", get(Self::outputs_address))
        .route("/parents/{inscription_id}", get(Self::parents))
        .route(
          "/parents/{inscription_id}/{page}",
          get(Self::parents_paginated),
        )
        .route("/preview/{inscription_id}", get(Self::preview))
        .route("/rare.txt", get(Self::rare_txt))
        .route("/rune/{rune}", get(Self::rune))
        .route("/runes", get(Self::runes))
        .route("/runes/{page}", get(Self::runes_paginated))
        .route("/sat/{sat}", get(Self::sat))
        .route("/satpoint/{satpoint}", get(Self::satpoint))
        .route("/satscard", get(Self::satscard))
        .route("/search", get(Self::search_by_query))
        .route("/search/{*query}", get(Self::search_by_path))
        .route("/static/{*path}", get(Self::static_asset))
        .route("/status", get(Self::status))
        .route("/tx/{txid}", get(Self::transaction))
        .route("/update", get(Self::update));

      // recursive endpoints
      let router = router
        .route("/blockhash", get(r::blockhash_string))
        .route("/blockhash/{height}", get(r::block_hash_from_height_string))
        .route("/blockheight", get(r::blockheight_string))
        .route("/blocktime", get(r::blocktime_string))
        .route("/r/blockhash", get(r::blockhash))
        .route("/r/blockhash/{height}", get(r::blockhash_at_height))
        .route("/r/blockheight", get(r::blockheight_string))
        .route("/r/blockinfo/{query}", get(r::blockinfo))
        .route("/r/blocktime", get(r::blocktime_string))
        .route(
          "/r/children/{inscription_id}/inscriptions",
          get(r::children_inscriptions),
        )
        .route(
          "/r/children/{inscription_id}/inscriptions/{page}",
          get(r::children_inscriptions_paginated),
        )
        .route("/r/parents/{inscription_id}", get(r::parents))
        .route(
          "/r/parents/{inscription_id}/{page}",
          get(r::parents_paginated),
        )
        .route(
          "/r/parents/{inscription_id}/inscriptions",
          get(r::parent_inscriptions),
        )
        .route(
          "/r/parents/{inscription_id}/inscriptions/{page}",
          get(r::parent_inscriptions_paginated),
        )
        .route("/r/sat/{sat_number}", get(r::sat))
        .route("/r/sat/{sat_number}/{page}", get(r::sat_paginated))
        .route("/r/tx/{txid}", get(r::tx))
        .route(
          "/r/undelegated-content/{inscription_id}",
          get(r::undelegated_content),
        )
        .route("/r/utxo/{outpoint}", get(r::utxo));

      let proxiable_routes = Router::new()
        .route("/content/{inscription_id}", get(r::content))
        .route("/r/children/{inscription_id}", get(r::children))
        .route(
          "/r/children/{inscription_id}/{page}",
          get(r::children_paginated),
        )
        .route("/r/inscription/{inscription_id}", get(r::inscription))
        .route("/r/metadata/{inscription_id}", get(r::metadata))
        .route("/r/sat/{sat_number}/at/{index}", get(r::sat_at_index))
        .route(
          "/r/sat/{sat_number}/at/{index}/content",
          get(r::sat_at_index_content),
        )
        .layer(axum::middleware::from_fn(Self::proxy_layer));

      let router = router.merge(proxiable_routes);

      let router = router
        .fallback(Self::fallback)
        .layer(Extension(index))
        .layer(Extension(server_config.clone()))
        .layer(Extension(settings.clone()))
        .layer(SetResponseHeaderLayer::if_not_present(
          header::CONTENT_SECURITY_POLICY,
          HeaderValue::from_static("default-src 'self'"),
        ))
        .layer(SetResponseHeaderLayer::overriding(
          header::STRICT_TRANSPORT_SECURITY,
          HeaderValue::from_static("max-age=31536000; includeSubDomains; preload"),
        ))
        .layer(
          CorsLayer::new()
            .allow_methods([http::Method::GET, http::Method::POST])
            .allow_headers([http::header::CONTENT_TYPE])
            .allow_origin(Any),
        )
        .layer(CompressionLayer::new())
        .with_state(server_config.clone());

      let router = if let Some((username, password)) = settings.credentials() {
        #[allow(deprecated)]
        router.layer(ValidateRequestHeaderLayer::basic(username, password))
      } else {
        router
      };

      match (self.http_port(), self.https_port()) {
        (Some(http_port), None) => {
          self
            .spawn(
              &settings,
              router,
              handle,
              http_port,
              SpawnConfig::Http,
              http_port_tx,
            )?
            .await??
        }
        (None, Some(https_port)) => {
          self
            .spawn(
              &settings,
              router,
              handle,
              https_port,
              SpawnConfig::Https(self.acceptor(&settings)?),
              None,
            )?
            .await??
        }
        (Some(http_port), Some(https_port)) => {
          let http_spawn_config = if self.redirect_http_to_https {
            SpawnConfig::Redirect(if https_port == 443 {
              format!("https://{}", acme_domains[0])
            } else {
              format!("https://{}:{https_port}", acme_domains[0])
            })
          } else {
            SpawnConfig::Http
          };

          let (http_result, https_result) = tokio::join!(
            self.spawn(
              &settings,
              router.clone(),
              handle.clone(),
              http_port,
              http_spawn_config,
              http_port_tx,
            )?,
            self.spawn(
              &settings,
              router,
              handle,
              https_port,
              SpawnConfig::Https(self.acceptor(&settings)?),
              None,
            )?
          );
          http_result.and(https_result)??;
        }
        (None, None) => unreachable!(),
      }

      Ok(None)
    })
  }

  fn spawn(
    &self,
    settings: &Settings,
    router: Router,
    handle: Handle<SocketAddr>,
    port: u16,
    config: SpawnConfig,
    port_tx: Option<std::sync::mpsc::Sender<u16>>,
  ) -> Result<task::JoinHandle<io::Result<()>>> {
    let address = match &self.address {
      Some(address) => address.as_str(),
      None => {
        if cfg!(test) || settings.integration_test() {
          "127.0.0.1"
        } else {
          "0.0.0.0"
        }
      }
    };

    let addr = (address, port)
      .to_socket_addrs()?
      .next()
      .ok_or_else(|| anyhow!("failed to get socket addrs"))?;

    let test = settings.integration_test() || cfg!(test);

    Ok(tokio::spawn(async move {
      let listener = tokio::net::TcpListener::bind(addr).await?.into_std()?;

      let addr = listener.local_addr()?;

      if !test {
        eprintln!(
          "Listening on {}://{addr}",
          match config {
            SpawnConfig::Https(_) => "https",
            _ => "http",
          }
        );
      }

      if let Some(tx) = port_tx {
        tx.send(addr.port()).unwrap();
      }

      match config {
        SpawnConfig::Https(acceptor) => {
          axum_server::from_tcp(listener)?
            .handle(handle)
            .acceptor(acceptor)
            .serve(router.into_make_service())
            .await
        }
        SpawnConfig::Redirect(destination) => {
          axum_server::from_tcp(listener)?
            .handle(handle)
            .serve(
              Router::new()
                .fallback(Self::redirect_http_to_https)
                .layer(Extension(destination))
                .into_make_service(),
            )
            .await
        }
        SpawnConfig::Http => {
          axum_server::from_tcp(listener)?
            .handle(handle)
            .serve(router.into_make_service())
            .await
        }
      }
    }))
  }

  fn acme_cache(acme_cache: Option<&PathBuf>, settings: &Settings) -> PathBuf {
    match acme_cache {
      Some(acme_cache) => acme_cache.clone(),
      None => settings.data_dir().join("acme-cache"),
    }
  }

  fn acme_domains(&self) -> Result<Vec<String>> {
    if !self.acme_domain.is_empty() {
      Ok(self.acme_domain.clone())
    } else {
      Ok(vec![
        System::host_name().ok_or(anyhow!("no hostname found"))?,
      ])
    }
  }

  fn http_port(&self) -> Option<u16> {
    if self.http || self.http_port.is_some() || (self.https_port.is_none() && !self.https) {
      Some(self.http_port.unwrap_or(80))
    } else {
      None
    }
  }

  fn https_port(&self) -> Option<u16> {
    if self.https || self.https_port.is_some() {
      Some(self.https_port.unwrap_or(443))
    } else {
      None
    }
  }

  fn acceptor(&self, settings: &Settings) -> Result<AxumAcceptor> {
    static RUSTLS_PROVIDER_INSTALLED: LazyLock<bool> = LazyLock::new(|| {
      rustls::crypto::ring::default_provider()
        .install_default()
        .is_ok()
    });

    let config = AcmeConfig::new(self.acme_domains()?)
      .contact(&self.acme_contact)
      .cache_option(Some(DirCache::new(Self::acme_cache(
        self.acme_cache.as_ref(),
        settings,
      ))))
      .directory(if cfg!(test) {
        LETS_ENCRYPT_STAGING_DIRECTORY
      } else {
        LETS_ENCRYPT_PRODUCTION_DIRECTORY
      });

    let mut state = config.state();

    ensure! {
      *RUSTLS_PROVIDER_INSTALLED,
      "failed to install rustls ring crypto provider",
    }

    let mut server_config = rustls::ServerConfig::builder()
      .with_no_client_auth()
      .with_cert_resolver(state.resolver());

    server_config.alpn_protocols = vec!["h2".into(), "http/1.1".into()];

    let acceptor = state.axum_acceptor(Arc::new(server_config));

    tokio::spawn(async move {
      while let Some(result) = state.next().await {
        match result {
          Ok(ok) => log::info!("ACME event: {ok:?}"),
          Err(err) => log::error!("ACME error: {err:?}"),
        }
      }
    });

    Ok(acceptor)
  }

  async fn proxy_layer(
    server_config: Extension<Arc<ServerConfig>>,
    request: http::Request<axum::body::Body>,
    next: axum::middleware::Next,
  ) -> ServerResult {
    let path = request.uri().path().to_owned();

    let response = next.run(request).await;

    if let Some(proxy) = &server_config.proxy {
      if response.status() == StatusCode::NOT_FOUND {
        return task::block_in_place(|| Server::proxy(proxy, &path));
      }

      // `/r/sat/<SAT_NUMBER>/at/<INDEX>` does not return a 404 when no
      // inscription is present, so we must deserialize and check the body.
      if SAT_AT_INDEX_PATH.is_match(&path) {
        let (parts, body) = response.into_parts();

        let bytes = axum::body::to_bytes(body, usize::MAX)
          .await
          .map_err(|err| anyhow!(err))?;

        if let Ok(api::SatInscription { id: None }) =
          serde_json::from_slice::<api::SatInscription>(&bytes)
        {
          return task::block_in_place(|| Server::proxy(proxy, &path));
        }

        return Ok(Response::from_parts(parts, axum::body::Body::from(bytes)));
      }
    }

    Ok(response)
  }

  fn index_height(index: &Index) -> ServerResult<Height> {
    index.block_height()?.ok_or_not_found(|| "genesis block")
  }

  async fn clock(Extension(index): Extension<Arc<Index>>) -> ServerResult {
    task::block_in_place(|| {
      Ok(
        (
          [(
            header::CONTENT_SECURITY_POLICY,
            HeaderValue::from_static("default-src 'unsafe-inline'"),
          )],
          ClockSvg::new(Self::index_height(&index)?),
        )
          .into_response(),
      )
    })
  }

  async fn fallback(Extension(index): Extension<Arc<Index>>, uri: Uri) -> ServerResult<Response> {
    task::block_in_place(|| {
      let path = urlencoding::decode(uri.path().trim_matches('/'))
        .map_err(|err| ServerError::BadRequest(err.to_string()))?;

      let prefix = if re::INSCRIPTION_ID.is_match(&path) || re::INSCRIPTION_NUMBER.is_match(&path) {
        "inscription"
      } else if re::RUNE_ID.is_match(&path) || re::SPACED_RUNE.is_match(&path) {
        "rune"
      } else if re::OUTPOINT.is_match(&path) {
        "output"
      } else if re::SATPOINT.is_match(&path) {
        "satpoint"
      } else if re::HASH.is_match(&path) {
        if index.block_header(path.parse().unwrap())?.is_some() {
          "block"
        } else {
          "tx"
        }
      } else if re::ADDRESS.is_match(&path) {
        "address"
      } else {
        return Ok(StatusCode::NOT_FOUND.into_response());
      };

      Ok(Redirect::to(&format!("/{prefix}/{path}")).into_response())
    })
  }

  async fn satscard(
    Extension(settings): Extension<Arc<Settings>>,
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    uri: Uri,
  ) -> ServerResult<Response> {
    #[derive(Debug, Deserialize)]
    struct Form {
      url: DeserializeFromStr<Url>,
    }

    if let Ok(form) = Query::<Form>::try_from_uri(&uri) {
      return if let Some(fragment) = form.url.0.fragment() {
        Ok(Redirect::to(&format!("/satscard?{fragment}")).into_response())
      } else if let Some(query) = form.url.0.query() {
        Ok(Redirect::to(&format!("/satscard?{query}")).into_response())
      } else {
        Err(ServerError::BadRequest(
          "satscard URL missing fragment".into(),
        ))
      };
    }

    let satscard = if let Some(query) = uri.query().filter(|query| !query.is_empty()) {
      let satscard = Satscard::from_query_parameters(settings.chain(), query).map_err(|err| {
        ServerError::BadRequest(format!("invalid satscard query parameters: {err}"))
      })?;

      let address_info = Self::address_info(&index, &satscard.address)?.map(
        |api::AddressInfo {
           outputs,
           inscriptions,
           sat_balance,
           runes_balances,
         }| AddressHtml {
          address: satscard.address.clone(),
          header: false,
          inscriptions,
          outputs,
          runes_balances,
          sat_balance,
        },
      );

      Some((satscard, address_info))
    } else {
      None
    };

    Ok(
      SatscardHtml { satscard }
        .page(server_config)
        .into_response(),
    )
  }

  async fn sat(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(DeserializeFromStr(sat)): Path<DeserializeFromStr<Sat>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let inscriptions = index.get_inscription_ids_by_sat(sat)?;
      let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| {
        inscriptions.first().and_then(|&first_inscription_id| {
          index
            .get_inscription_satpoint_by_id(first_inscription_id)
            .ok()
            .flatten()
        })
      });
      let blocktime = index.block_time(sat.height())?;

      let charms = sat.charms();

      let address = if let Some(satpoint) = satpoint {
        if satpoint.outpoint == unbound_outpoint() {
          None
        } else {
          let tx = index
            .get_transaction(satpoint.outpoint.txid)?
            .context("could not get transaction for sat")?;

          let tx_out = tx
            .output
            .get::<usize>(satpoint.outpoint.vout.try_into().unwrap())
            .context("could not get vout for sat")?;

          server_config
            .chain
            .address_from_script(&tx_out.script_pubkey)
            .ok()
        }
      } else {
        None
      };

      Ok(if accept_json {
        Json(api::Sat {
          address: address.map(|address| address.to_string()),
          block: sat.height().0,
          charms: Charm::charms(charms),
          cycle: sat.cycle(),
          decimal: sat.decimal().to_string(),
          degree: sat.degree().to_string(),
          epoch: sat.epoch().0,
          inscriptions,
          name: sat.name(),
          number: sat.0,
          offset: sat.third(),
          percentile: sat.percentile(),
          period: sat.period(),
          rarity: sat.rarity(),
          satpoint,
          timestamp: blocktime.timestamp().timestamp(),
        })
        .into_response()
      } else {
        SatHtml {
          address,
          blocktime,
          inscriptions,
          sat,
          satpoint,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn ordinal(Path(sat): Path<String>) -> Redirect {
    Redirect::to(&format!("/sat/{sat}"))
  }

  async fn output(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(outpoint): Path<OutPoint>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (output_info, txout) = index
        .get_output_info(outpoint)?
        .ok_or_not_found(|| format!("output {outpoint}"))?;

      Ok(if accept_json {
        Json(output_info).into_response()
      } else {
        OutputHtml {
          chain: server_config.chain,
          confirmations: output_info.confirmations,
          inscriptions: output_info.inscriptions,
          outpoint,
          output: txout,
          runes: output_info.runes,
          sat_ranges: output_info.sat_ranges,
          spent: output_info.spent,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn satpoint(
    Extension(index): Extension<Arc<Index>>,
    Path(satpoint): Path<SatPoint>,
  ) -> ServerResult<Redirect> {
    task::block_in_place(|| {
      let (output_info, _) = index
        .get_output_info(satpoint.outpoint)?
        .ok_or_not_found(|| format!("satpoint {satpoint}"))?;

      let Some(ranges) = output_info.sat_ranges else {
        return Err(ServerError::NotFound("sat index required".into()));
      };

      let mut total = 0;
      for (start, end) in ranges {
        let size = end - start;
        if satpoint.offset < total + size {
          let sat = start + satpoint.offset - total;

          return Ok(Redirect::to(&format!("/sat/{sat}")));
        }
        total += size;
      }

      Err(ServerError::NotFound(format!(
        "satpoint {satpoint} not found"
      )))
    })
  }

  async fn outputs(
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Json(outputs): Json<Vec<OutPoint>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      Ok(if accept_json {
        let mut response = Vec::new();
        for outpoint in outputs {
          let (output_info, _) = index
            .get_output_info(outpoint)?
            .ok_or_not_found(|| format!("output {outpoint}"))?;

          response.push(output_info);
        }
        Json(response).into_response()
      } else {
        StatusCode::NOT_FOUND.into_response()
      })
    })
  }

  async fn outputs_address(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Path(address): Path<Address<NetworkUnchecked>>,
    Query(query): Query<OutputsQuery>,
  ) -> ServerResult {
    task::block_in_place(|| {
      if !index.has_address_index() {
        return Err(ServerError::NotFound(
          "this server has no address index".to_string(),
        ));
      }

      if !accept_json {
        return Ok(StatusCode::NOT_FOUND.into_response());
      }

      let output_type = query.ty.unwrap_or_default();

      if output_type != OutputType::Any {
        if !index.has_rune_index() {
          return Err(ServerError::BadRequest(
            "this server has no runes index".to_string(),
          ));
        }

        if !index.has_inscription_index() {
          return Err(ServerError::BadRequest(
            "this server has no inscriptions index".to_string(),
          ));
        }
      }

      let address = address
        .require_network(server_config.chain.network())
        .map_err(|err| ServerError::BadRequest(err.to_string()))?;

      let outputs = index.get_address_info(&address)?;

      let mut response = Vec::new();
      for output in outputs.into_iter() {
        let include = match output_type {
          OutputType::Any => true,
          OutputType::Cardinal => {
            index
              .get_inscriptions_on_output_with_satpoints(output)?
              .unwrap_or_default()
              .is_empty()
              && index
                .get_rune_balances_for_output(output)?
                .unwrap_or_default()
                .is_empty()
          }
          OutputType::Inscribed => !index
            .get_inscriptions_on_output_with_satpoints(output)?
            .unwrap_or_default()
            .is_empty(),
          OutputType::Runic => !index
            .get_rune_balances_for_output(output)?
            .unwrap_or_default()
            .is_empty(),
        };

        if include {
          let (output_info, _) = index
            .get_output_info(output)?
            .ok_or_not_found(|| format!("output {output}"))?;

          response.push(output_info);
        }
      }

      Ok(Json(response).into_response())
    })
  }

  async fn rare_txt(Extension(index): Extension<Arc<Index>>) -> ServerResult<RareTxt> {
    task::block_in_place(|| Ok(RareTxt(index.rare_sat_satpoints()?)))
  }

  async fn rune(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(DeserializeFromStr(rune_query)): Path<DeserializeFromStr<query::Rune>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      if !index.has_rune_index() {
        return Err(ServerError::NotFound(
          "this server has no rune index".to_string(),
        ));
      }

      let rune = match rune_query {
        query::Rune::Spaced(spaced_rune) => spaced_rune.rune,
        query::Rune::Id(rune_id) => index
          .get_rune_by_id(rune_id)?
          .ok_or_not_found(|| format!("rune {rune_id}"))?,
        query::Rune::Number(number) => index
          .get_rune_by_number(usize::try_from(number).unwrap())?
          .ok_or_not_found(|| format!("rune number {number}"))?,
      };

      let Some((id, entry, parent)) = index.rune(rune)? else {
        return Ok(if accept_json {
          StatusCode::NOT_FOUND.into_response()
        } else {
          let unlock = if let Some(height) = rune.unlock_height(server_config.chain.network()) {
            Some((height, index.block_time(height)?))
          } else {
            None
          };

          (
            StatusCode::NOT_FOUND,
            RuneNotFoundHtml { rune, unlock }.page(server_config),
          )
            .into_response()
        });
      };

      let block_height = index.block_height()?.unwrap_or(Height(0));

      let mintable = entry.mintable((block_height.n() + 1).into()).is_ok();

      Ok(if accept_json {
        Json(api::Rune {
          entry,
          id,
          mintable,
          parent,
        })
        .into_response()
      } else {
        RuneHtml {
          entry,
          id,
          mintable,
          parent,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn runes(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    accept_json: AcceptJson,
  ) -> ServerResult<Response> {
    Self::runes_paginated(
      Extension(server_config),
      Extension(index),
      Path(0),
      accept_json,
    )
    .await
  }

  async fn runes_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(page_index): Path<usize>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (entries, more) = index.runes_paginated(50, page_index)?;

      let prev = page_index.checked_sub(1);

      let next = more.then_some(page_index + 1);

      Ok(if accept_json {
        Json(RunesHtml {
          entries,
          more,
          prev,
          next,
        })
        .into_response()
      } else {
        RunesHtml {
          entries,
          more,
          prev,
          next,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn home(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
  ) -> ServerResult<PageHtml<HomeHtml>> {
    task::block_in_place(|| {
      Ok(
        HomeHtml {
          inscriptions: index.get_home_inscriptions()?,
        }
        .page(server_config),
      )
    })
  }

  async fn blocks(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let blocks = index.blocks(100)?;
      let mut featured_blocks = BTreeMap::new();
      for (height, hash) in blocks.iter().take(5) {
        let (inscriptions, _total_num) =
          index.get_highest_paying_inscriptions_in_block(*height, 8)?;

        featured_blocks.insert(*hash, inscriptions);
      }

      Ok(if accept_json {
        Json(api::Blocks::new(blocks, featured_blocks)).into_response()
      } else {
        BlocksHtml::new(blocks, featured_blocks)
          .page(server_config)
          .into_response()
      })
    })
  }

  async fn install_script() -> Redirect {
    Redirect::to("https://raw.githubusercontent.com/ordinals/ord/master/install.sh")
  }

  async fn offer(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    offer: String,
  ) -> ServerResult {
    if !server_config.accept_offers {
      return Err(ServerError::NotFound(
        "this server does not accept offers".into(),
      ));
    }

    task::block_in_place(|| {
      let offer = base64_decode(&offer)
        .map_err(|err| ServerError::BadRequest(format!("failed to base64 decode PSBT: {err}")))?;

      let offer = Psbt::deserialize(&offer)
        .map_err(|err| ServerError::BadRequest(format!("invalid offer PSBT: {err}")))?;

      index.insert_offer(offer).map_err(ServerError::Internal)?;

      Ok("".into_response())
    })
  }

  async fn offers(
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    if !accept_json {
      return Ok(StatusCode::NOT_FOUND.into_response());
    }

    task::block_in_place(|| {
      Ok(
        Json(api::Offers {
          offers: index
            .get_offers()?
            .into_iter()
            .map(|offer| base64_encode(&offer))
            .collect(),
        })
        .into_response(),
      )
    })
  }

  async fn address(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(address): Path<Address<NetworkUnchecked>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let address = address
        .require_network(server_config.chain.network())
        .map_err(|err| ServerError::BadRequest(err.to_string()))?;

      let Some(info) = Self::address_info(&index, &address)? else {
        return Err(ServerError::NotFound(
          "this server has no address index".to_string(),
        ));
      };

      Ok(if accept_json {
        Json(info).into_response()
      } else {
        let api::AddressInfo {
          sat_balance,
          outputs,
          inscriptions,
          runes_balances,
        } = info;

        AddressHtml {
          address,
          header: true,
          inscriptions,
          outputs,
          runes_balances,
          sat_balance,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  fn address_info(index: &Index, address: &Address) -> ServerResult<Option<api::AddressInfo>> {
    if !index.has_address_index() {
      return Ok(None);
    }

    let mut outputs = index.get_address_info(address)?;

    outputs.sort();

    let sat_balance = index.get_sat_balances_for_outputs(&outputs)?;

    let inscriptions = index.get_inscriptions_for_outputs(&outputs)?;

    let runes_balances = index.get_aggregated_rune_balances_for_outputs(&outputs)?;

    Ok(Some(api::AddressInfo {
      sat_balance,
      outputs,
      inscriptions,
      runes_balances,
    }))
  }

  async fn block(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Block>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (block, height) = match query {
        query::Block::Height(height) => {
          let block = index
            .get_block_by_height(height)?
            .ok_or_not_found(|| format!("block {height}"))?;

          (block, height)
        }
        query::Block::Hash(hash) => {
          let info = index
            .block_header_info(hash)?
            .ok_or_not_found(|| format!("block {hash}"))?;

          let block = index
            .get_block_by_hash(hash)?
            .ok_or_not_found(|| format!("block {hash}"))?;

          (block, u32::try_from(info.height).unwrap())
        }
      };

      let runes = index.get_runes_in_block(u64::from(height))?;
      Ok(if accept_json {
        let inscriptions = index.get_inscriptions_in_block(height)?;
        Json(api::Block::new(
          block,
          Height(height),
          Self::index_height(&index)?,
          inscriptions,
          runes,
        ))
        .into_response()
      } else {
        let (featured_inscriptions, total_num) =
          index.get_highest_paying_inscriptions_in_block(height, 8)?;
        BlockHtml::new(
          block,
          Height(height),
          Self::index_height(&index)?,
          total_num,
          featured_inscriptions,
          runes,
        )
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn transaction(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(txid): Path<Txid>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let transaction = index
        .get_transaction(txid)?
        .ok_or_not_found(|| format!("transaction {txid}"))?;

      let inscription_count = index.inscription_count(txid)?;

      Ok(if accept_json {
        Json(api::Transaction {
          chain: server_config.chain,
          etching: index.get_etching(txid)?,
          inscription_count,
          transaction,
          txid,
        })
        .into_response()
      } else {
        TransactionHtml {
          chain: server_config.chain,
          etching: index.get_etching(txid)?,
          inscription_count,
          transaction,
          txid,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn decode(
    Extension(index): Extension<Arc<Index>>,
    Path(txid): Path<Txid>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let transaction = index
        .get_transaction(txid)?
        .ok_or_not_found(|| format!("transaction {txid}"))?;

      let inscriptions = ParsedEnvelope::from_transaction(&transaction);
      let runestone = Runestone::decipher(&transaction);

      Ok(if accept_json {
        Json(api::Decode {
          inscriptions,
          runestone,
        })
        .into_response()
      } else {
        StatusCode::NOT_FOUND.into_response()
      })
    })
  }

  async fn update(
    Extension(settings): Extension<Arc<Settings>>,
    Extension(index): Extension<Arc<Index>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      if settings.integration_test() {
        index.update()?;
        Ok(index.block_count()?.to_string().into_response())
      } else {
        Ok(StatusCode::NOT_FOUND.into_response())
      }
    })
  }

  async fn status(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      Ok(if accept_json {
        Json(index.status(server_config.json_api_enabled)?).into_response()
      } else {
        index
          .status(server_config.json_api_enabled)?
          .page(server_config)
          .into_response()
      })
    })
  }

  async fn search_by_query(
    Extension(index): Extension<Arc<Index>>,
    Query(search): Query<Search>,
  ) -> ServerResult<Redirect> {
    Self::search(index, search.query).await
  }

  async fn search_by_path(
    Extension(index): Extension<Arc<Index>>,
    Path(search): Path<Search>,
  ) -> ServerResult<Redirect> {
    Self::search(index, search.query).await
  }

  async fn search(index: Arc<Index>, query: String) -> ServerResult<Redirect> {
    task::block_in_place(|| {
      let query = query.trim();

      if re::HASH.is_match(query) {
        if index.block_header(query.parse().unwrap())?.is_some() {
          Ok(Redirect::to(&format!("/block/{query}")))
        } else {
          Ok(Redirect::to(&format!("/tx/{query}")))
        }
      } else if re::OUTPOINT.is_match(query) {
        Ok(Redirect::to(&format!("/output/{query}")))
      } else if re::INSCRIPTION_ID.is_match(query) || re::INSCRIPTION_NUMBER.is_match(query) {
        Ok(Redirect::to(&format!("/inscription/{query}")))
      } else if let Some(captures) = re::COINKITE_SATSCARD_URL.captures(query) {
        Ok(Redirect::to(&format!(
          "/satscard?{}",
          &captures["parameters"]
        )))
      } else if let Some(captures) = re::ORDINALS_SATSCARD_URL.captures(query) {
        Ok(Redirect::to(&format!("/satscard?{}", &captures["query"])))
      } else if re::SPACED_RUNE.is_match(query) {
        Ok(Redirect::to(&format!("/rune/{query}")))
      } else if re::RUNE_ID.is_match(query) {
        let id = query
          .parse::<RuneId>()
          .map_err(|err| ServerError::BadRequest(err.to_string()))?;

        let rune = index.get_rune_by_id(id)?.ok_or_not_found(|| "rune ID")?;

        Ok(Redirect::to(&format!("/rune/{rune}")))
      } else if re::ADDRESS.is_match(query) {
        Ok(Redirect::to(&format!("/address/{query}")))
      } else if re::SATPOINT.is_match(query) {
        Ok(Redirect::to(&format!("/satpoint/{query}")))
      } else {
        Ok(Redirect::to(&format!("/sat/{query}")))
      }
    })
  }

  async fn favicon() -> ServerResult {
    Ok(
      Self::static_asset(Path("/favicon.png".to_string()))
        .await
        .into_response(),
    )
  }

  async fn feed(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      let mut builder = rss::ChannelBuilder::default();

      let chain = server_config.chain;
      match chain {
        Chain::Mainnet => builder.title("Inscriptions".to_string()),
        _ => builder.title(format!("Inscriptions – {chain:?}")),
      };

      builder.generator(Some("ord".to_string()));

      for (number, id) in index.get_feed_inscriptions(300)? {
        builder.item(
          rss::ItemBuilder::default()
            .title(Some(format!("Inscription {number}")))
            .link(Some(format!("/inscription/{id}")))
            .guid(Some(rss::Guid {
              value: format!("/inscription/{id}"),
              permalink: true,
            }))
            .build(),
        );
      }

      Ok(
        (
          [
            (header::CONTENT_TYPE, "application/rss+xml"),
            (
              header::CONTENT_SECURITY_POLICY,
              "default-src 'unsafe-inline'",
            ),
          ],
          builder.build().to_string(),
        )
          .into_response(),
      )
    })
  }

  async fn static_asset(Path(path): Path<String>) -> ServerResult {
    let content = StaticAssets::get(if let Some(stripped) = path.strip_prefix('/') {
      stripped
    } else {
      &path
    })
    .ok_or_not_found(|| format!("asset {path}"))?;

    let mime = mime_guess::from_path(path).first_or_octet_stream();

    Ok(
      Response::builder()
        .header(header::CONTENT_TYPE, mime.as_ref())
        .body(content.data.into())
        .unwrap(),
    )
  }

  async fn block_count(Extension(index): Extension<Arc<Index>>) -> ServerResult<String> {
    task::block_in_place(|| Ok(index.block_count()?.to_string()))
  }

  async fn input(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(path): Path<(u32, usize, usize)>,
  ) -> ServerResult<PageHtml<InputHtml>> {
    task::block_in_place(|| {
      let not_found = || format!("input /{}/{}/{}", path.0, path.1, path.2);

      let block = index
        .get_block_by_height(path.0)?
        .ok_or_not_found(not_found)?;

      let transaction = block
        .txdata
        .into_iter()
        .nth(path.1)
        .ok_or_not_found(not_found)?;

      let input = transaction
        .input
        .into_iter()
        .nth(path.2)
        .ok_or_not_found(not_found)?;

      Ok(InputHtml { path, input }.page(server_config))
    })
  }

  async fn faq() -> Redirect {
    Redirect::to("https://docs.ordinals.com/faq")
  }

  async fn bounties() -> Redirect {
    Redirect::to("https://docs.ordinals.com/bounties")
  }

  async fn preview(
    Extension(index): Extension<Arc<Index>>,
    Extension(settings): Extension<Arc<Settings>>,
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Path(inscription_id): Path<InscriptionId>,
    accept_encoding: AcceptEncoding,
  ) -> ServerResult {
    task::block_in_place(|| {
      if settings.is_hidden(inscription_id) {
        return Ok(PreviewUnknownHtml.into_response());
      }

      let mut inscription = index
        .get_inscription_by_id(inscription_id)?
        .ok_or_not_found(|| format!("inscription {inscription_id}"))?;

      let inscription_number = index
        .get_inscription_entry(inscription_id)?
        .ok_or_not_found(|| format!("inscription {inscription_id}"))?
        .inscription_number;

      if let Some(delegate) = inscription.delegate() {
        inscription = index
          .get_inscription_by_id(delegate)?
          .ok_or_not_found(|| format!("delegate {inscription_id}"))?
      }

      let media = inscription.media();

      if let Media::Iframe = media {
        return Ok(
          r::content_response(inscription, accept_encoding, &server_config, true)?
            .ok_or_not_found(|| format!("inscription {inscription_id} content"))?
            .into_response(),
        );
      }

      let content_security_policy = server_config.preview_content_security_policy(media)?;

      match media {
        Media::Audio => Ok(
          (
            content_security_policy,
            PreviewAudioHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Code(language) => Ok(
          (
            content_security_policy,
            PreviewCodeHtml {
              inscription_id,
              language,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Font => Ok(
          (
            content_security_policy,
            PreviewFontHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Iframe => unreachable!(),
        Media::Image(image_rendering) => Ok(
          (
            content_security_policy,
            PreviewImageHtml {
              image_rendering,
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Markdown => Ok(
          (
            content_security_policy,
            PreviewMarkdownHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Model => Ok(
          (
            content_security_policy,
            PreviewModelHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Pdf => Ok(
          (
            content_security_policy,
            PreviewPdfHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Text => Ok(
          (
            content_security_policy,
            PreviewTextHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Unknown => Ok((content_security_policy, PreviewUnknownHtml).into_response()),
        Media::Video => Ok(
          (
            content_security_policy,
            PreviewVideoHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
      }
    })
  }

  async fn item(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path((DeserializeFromStr(query), i)): Path<(DeserializeFromStr<query::Inscription>, usize)>,
  ) -> ServerResult<PageHtml<ItemHtml>> {
    task::block_in_place(|| {
      if let query::Inscription::Sat(_) = query
        && !index.has_sat_index()
      {
        return Err(ServerError::NotFound("sat index required".into()));
      }

      let (info, _txout, inscription) = index
        .inscription_info(query, None)?
        .ok_or_not_found(|| format!("inscription {query}"))?;

      let properties = inscription.properties();

      let item = properties
        .gallery
        .get(i)
        .ok_or_not_found(|| format!("gallery {query} item {i}"))?
        .clone();

      Ok(
        ItemHtml {
          gallery_id: info.id,
          gallery_number: info.number,
          i,
          item,
        }
        .page(server_config),
      )
    })
  }

  async fn inscription(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Inscription>>,
  ) -> ServerResult {
    Self::inscription_inner(server_config, &index, accept_json, query, None).await
  }

  async fn inscription_child(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Path((DeserializeFromStr(query), child)): Path<(DeserializeFromStr<query::Inscription>, usize)>,
  ) -> ServerResult {
    Self::inscription_inner(server_config, &index, accept_json, query, Some(child)).await
  }

  async fn inscription_inner(
    server_config: Arc<ServerConfig>,
    index: &Index,
    accept_json: bool,
    query: query::Inscription,
    child: Option<usize>,
  ) -> ServerResult {
    task::block_in_place(|| {
      if let query::Inscription::Sat(_) = query
        && !index.has_sat_index()
      {
        return Err(ServerError::NotFound("sat index required".into()));
      }

      let inscription_info = index.inscription_info(query, child)?;

      Ok(if accept_json {
        let status_code = if inscription_info.is_none() {
          StatusCode::NOT_FOUND
        } else {
          StatusCode::OK
        };

        (status_code, Json(inscription_info.map(|info| info.0))).into_response()
      } else {
        let (info, txout, inscription) =
          inscription_info.ok_or_not_found(|| format!("inscription {query}"))?;

        let properties = inscription.properties();

        InscriptionHtml {
          chain: server_config.chain,
          charms: Charm::Vindicated.unset(info.charms.iter().fold(0, |mut acc, charm| {
            charm.set(&mut acc);
            acc
          })),
          child_count: info.child_count,
          children: info.children,
          fee: info.fee,
          height: info.height,
          id: info.id,
          inscription,
          next: info.next,
          number: info.number,
          output: txout,
          parents: info.parents,
          previous: info.previous,
          properties,
          rune: info.rune,
          sat: info.sat,
          satpoint: info.satpoint,
          timestamp: Utc.timestamp_opt(info.timestamp, 0).unwrap(),
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn inscriptions_json(
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Json(inscriptions): Json<Vec<InscriptionId>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      Ok(if accept_json {
        let mut response = Vec::new();
        for inscription in inscriptions {
          let query = query::Inscription::Id(inscription);
          let (info, _, _) = index
            .inscription_info(query, None)?
            .ok_or_not_found(|| format!("inscription {query}"))?;

          response.push(info);
        }

        Json(response).into_response()
      } else {
        StatusCode::NOT_FOUND.into_response()
      })
    })
  }

  async fn missing(
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Json(inscription_ids): Json<Vec<InscriptionId>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      Ok(if accept_json {
        Json(index.missing_inscriptions(&inscription_ids)?).into_response()
      } else {
        StatusCode::NOT_FOUND.into_response()
      })
    })
  }

  async fn collections(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
  ) -> ServerResult {
    Self::collections_paginated(Extension(server_config), Extension(index), Path(0)).await
  }

  async fn collections_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(page_index): Path<usize>,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (collections, more_collections) =
        index.get_collections_paginated(PAGE_SIZE, page_index)?;

      let prev = page_index.checked_sub(1);

      let next = more_collections.then_some(page_index + 1);

      Ok(
        CollectionsHtml {
          inscriptions: collections,
          prev,
          next,
        }
        .page(server_config)
        .into_response(),
      )
    })
  }

  async fn galleries(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    accept_json: AcceptJson,
  ) -> ServerResult {
    Self::galleries_paginated(
      Extension(server_config),
      Extension(index),
      Path(0),
      accept_json,
    )
    .await
  }

  async fn galleries_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(page_index): Path<u32>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (galleries, more) = index.get_galleries_paginated(PAGE_SIZE, page_index.into_usize())?;

      let prev = page_index.checked_sub(1);

      let next = more.then_some(page_index + 1);

      Ok(if accept_json {
        Json(api::Inscriptions {
          ids: galleries,
          page_index,
          more,
        })
        .into_response()
      } else {
        GalleriesHtml {
          inscriptions: galleries,
          prev,
          next,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn gallery(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(inscription_id): Path<InscriptionId>,
  ) -> ServerResult<PageHtml<GalleryHtml>> {
    Self::gallery_paginated(
      Extension(server_config),
      Extension(index),
      Path((inscription_id, 0)),
    )
    .await
  }

  async fn gallery_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path((id, page)): Path<(InscriptionId, usize)>,
  ) -> ServerResult<PageHtml<GalleryHtml>> {
    task::block_in_place(|| {
      let inscription = index
        .get_inscription_by_id(id)?
        .ok_or_not_found(|| format!("inscription {id}"))?;

      let number = index
        .get_inscription_entry(id)?
        .ok_or_not_found(|| format!("inscription {id}"))?
        .inscription_number;

      let properties = inscription.properties();

      let mut items = properties
        .gallery
        .iter()
        .enumerate()
        .skip(page.saturating_mul(PAGE_SIZE))
        .take(PAGE_SIZE.saturating_add(1))
        .map(|(i, item)| (i, item.id()))
        .collect::<Vec<(usize, InscriptionId)>>();

      let more = items.len() > PAGE_SIZE;

      if more {
        items.pop();
      }

      let prev_page = page.checked_sub(1);
      let next_page = more.then_some(page + 1);

      Ok(
        GalleryHtml {
          id,
          number,
          items,
          prev_page,
          next_page,
        }
        .page(server_config),
      )
    })
  }

  async fn children(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(inscription_id): Path<InscriptionId>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    Self::children_paginated(
      Extension(server_config),
      Extension(index),
      Path((inscription_id, 0)),
      AcceptJson(accept_json),
    )
    .await
  }

  async fn children_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path((parent, page)): Path<(InscriptionId, usize)>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let entry = index
        .get_inscription_entry(parent)?
        .ok_or_not_found(|| format!("inscription {parent}"))?;

      let parent_number = entry.inscription_number;

      let (children, more_children) =
        index.get_children_by_sequence_number_paginated(entry.sequence_number, PAGE_SIZE, page)?;

      let prev_page = page.checked_sub(1);

      let next_page = more_children.then_some(page + 1);

      Ok(if accept_json {
        Json(api::Children {
          ids: children,
          more: more_children,
          page,
        })
        .into_response()
      } else {
        ChildrenHtml {
          parent,
          parent_number,
          children,
          prev_page,
          next_page,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn inscriptions(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    accept_json: AcceptJson,
  ) -> ServerResult {
    Self::inscriptions_paginated(
      Extension(server_config),
      Extension(index),
      Path(0),
      accept_json,
    )
    .await
  }

  async fn inscriptions_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(page_index): Path<u32>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (inscriptions, more) = index.get_inscriptions_paginated(100, page_index)?;

      let prev = page_index.checked_sub(1);

      let next = more.then_some(page_index + 1);

      Ok(if accept_json {
        Json(api::Inscriptions {
          ids: inscriptions,
          page_index,
          more,
        })
        .into_response()
      } else {
        InscriptionsHtml {
          inscriptions,
          next,
          prev,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn inscriptions_in_block(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(block_height): Path<u32>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    Self::inscriptions_in_block_paginated(
      Extension(server_config),
      Extension(index),
      Path((block_height, 0)),
      AcceptJson(accept_json),
    )
    .await
  }

  async fn inscriptions_in_block_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path((block_height, page_index)): Path<(u32, u32)>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let mut inscriptions = index
        .get_inscriptions_in_block(block_height)?
        .into_iter()
        .skip(page_index.into_usize().saturating_mul(PAGE_SIZE))
        .take(PAGE_SIZE.saturating_add(1))
        .collect::<Vec<InscriptionId>>();

      let more = inscriptions.len() > PAGE_SIZE;

      if more {
        inscriptions.pop();
      }

      Ok(if accept_json {
        Json(api::Inscriptions {
          ids: inscriptions,
          page_index,
          more,
        })
        .into_response()
      } else {
        InscriptionsBlockHtml::new(
          block_height,
          index.block_height()?.unwrap_or(Height(0)).n(),
          inscriptions,
          more,
          page_index,
        )
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn parents(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(inscription_id): Path<InscriptionId>,
  ) -> ServerResult<Response> {
    Self::parents_paginated(
      Extension(server_config),
      Extension(index),
      Path((inscription_id, 0)),
    )
    .await
  }

  async fn parents_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path((id, page)): Path<(InscriptionId, usize)>,
  ) -> ServerResult<Response> {
    task::block_in_place(|| {
      let child = index
        .get_inscription_entry(id)?
        .ok_or_not_found(|| format!("inscription {id}"))?;

      let (parents, more) =
        index.get_parents_by_sequence_number_paginated(child.parents, PAGE_SIZE, page)?;

      let prev_page = page.checked_sub(1);

      let next_page = more.then_some(page + 1);

      Ok(
        ParentsHtml {
          id,
          number: child.inscription_number,
          parents,
          prev_page,
          next_page,
        }
        .page(server_config)
        .into_response(),
      )
    })
  }

  fn proxy(proxy: &Url, path: &str) -> ServerResult<Response> {
    let response = reqwest::blocking::Client::new()
      .get(format!("{}{}", proxy, &path[1..]))
      .send()
      .map_err(|err| anyhow!(err))?;

    let status = response.status();

    let mut headers = response.headers().clone();

    headers.insert(
      header::CONTENT_SECURITY_POLICY,
      HeaderValue::from_str(&format!(
        "default-src 'self' {proxy} 'unsafe-eval' 'unsafe-inline' data: blob:"
      ))
      .map_err(|err| ServerError::Internal(Error::from(err)))?,
    );

    Ok(
      (
        status,
        headers,
        response.bytes().map_err(|err| anyhow!(err))?,
      )
        .into_response(),
    )
  }

  async fn redirect_http_to_https(
    Extension(mut destination): Extension<String>,
    uri: Uri,
  ) -> Redirect {
    if let Some(path_and_query) = uri.path_and_query() {
      destination.push_str(path_and_query.as_str());
    }

    Redirect::to(&destination)
  }
}

#[cfg(test)]
mod tests {
  use {
    super::*,
    reqwest::{
      StatusCode, Url,
      header::{self, HeaderMap},
    },
    serde::de::DeserializeOwned,
    tempfile::TempDir,
  };

  const RUNE: u128 = 99246114928149462;

  #[derive(Default)]
  struct Builder {
    core: Option<mockcore::Handle>,
    config: String,
    ord_args: BTreeMap<String, Option<String>>,
    server_args: BTreeMap<String, Option<String>>,
  }

  impl Builder {
    fn core(self, core: mockcore::Handle) -> Self {
      Self {
        core: Some(core),
        ..self
      }
    }

    fn ord_option(mut self, option: &str, value: &str) -> Self {
      self.ord_args.insert(option.into(), Some(value.into()));
      self
    }

    fn ord_flag(mut self, flag: &str) -> Self {
      self.ord_args.insert(flag.into(), None);
      self
    }

    fn server_option(mut self, option: &str, value: &str) -> Self {
      self.server_args.insert(option.into(), Some(value.into()));
      self
    }

    fn server_flag(mut self, flag: &str) -> Self {
      self.server_args.insert(flag.into(), None);
      self
    }

    fn chain(self, chain: Chain) -> Self {
      self.ord_option("--chain", &chain.to_string())
    }

    fn config(self, config: &str) -> Self {
      Self {
        config: config.into(),
        ..self
      }
    }

    fn build(self) -> TestServer {
      let core = self.core.unwrap_or_else(|| {
        mockcore::builder()
          .network(
            self
              .ord_args
              .get("--chain")
              .map(|chain| chain.as_ref().unwrap().parse::<Chain>().unwrap())
              .unwrap_or_default()
              .network(),
          )
          .build()
      });

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

      let cookiefile = tempdir.path().join("cookie");

      fs::write(&cookiefile, "username:password").unwrap();

      let mut args = vec!["ord".to_string()];

      args.push("--bitcoin-rpc-url".into());
      args.push(core.url());

      args.push("--cookie-file".into());
      args.push(cookiefile.to_str().unwrap().into());

      args.push("--datadir".into());
      args.push(tempdir.path().to_str().unwrap().into());

      if !self.ord_args.contains_key("--chain") {
        args.push("--chain".into());
        args.push(core.network());
      }

      for (arg, value) in self.ord_args {
        args.push(arg);

        if let Some(value) = value {
          args.push(value);
        }
      }

      args.push("server".into());

      args.push("--address".into());
      args.push("127.0.0.1".into());

      args.push("--http-port".into());
      args.push("0".to_string());

      args.push("--polling-interval".into());
      args.push("100ms".into());

      for (arg, value) in self.server_args {
        args.push(arg);

        if let Some(value) = value {
          args.push(value);
        }
      }

      let arguments = Arguments::try_parse_from(args).unwrap();

      let Subcommand::Server(server) = arguments.subcommand else {
        panic!("unexpected subcommand: {:?}", arguments.subcommand);
      };

      let settings = Settings::from_options(arguments.options)
        .or(serde_yaml::from_str::<Settings>(&self.config).unwrap())
        .or_defaults()
        .unwrap();

      let index = Arc::new(Index::open(&settings).unwrap());
      let ord_server_handle = Handle::new();

      let (tx, rx) = std::sync::mpsc::channel();

      {
        let index = index.clone();
        let ord_server_handle = ord_server_handle.clone();
        thread::spawn(|| {
          server
            .run(settings, index, ord_server_handle, Some(tx))
            .unwrap()
        });
      }

      while index.statistic(crate::index::Statistic::Commits) == 0 {
        thread::sleep(Duration::from_millis(50));
      }

      let port = rx.recv().unwrap();

      TestServer {
        core,
        index,
        ord_server_handle,
        tempdir,
        url: Url::parse(&format!("http://127.0.0.1:{port}")).unwrap(),
      }
    }

    fn https(self) -> Self {
      self.server_flag("--https")
    }

    fn index_addresses(self) -> Self {
      self.ord_flag("--index-addresses")
    }

    fn index_runes(self) -> Self {
      self.ord_flag("--index-runes")
    }

    fn index_sats(self) -> Self {
      self.ord_flag("--index-sats")
    }

    fn redirect_http_to_https(self) -> Self {
      self.server_flag("--redirect-http-to-https")
    }
  }

  struct TestServer {
    core: mockcore::Handle,
    index: Arc<Index>,
    ord_server_handle: Handle<SocketAddr>,
    #[allow(unused)]
    tempdir: TempDir,
    url: Url,
  }

  impl TestServer {
    fn builder() -> Builder {
      Default::default()
    }

    fn new() -> Self {
      Builder::default().build()
    }

    #[track_caller]
    pub(crate) fn etch(
      &self,
      runestone: Runestone,
      outputs: usize,
      witness: Option<Witness>,
    ) -> (Txid, RuneId) {
      let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap();

      self.mine_blocks(1);

      self.core.broadcast_tx(TransactionTemplate {
        inputs: &[(block_count, 0, 0, Default::default())],
        p2tr: true,
        ..default()
      });

      self.mine_blocks((Runestone::COMMIT_CONFIRMATIONS - 1).into());

      let witness = witness.unwrap_or_else(|| {
        let tapscript = script::Builder::new()
          .push_slice::<&PushBytes>(
            runestone
              .etching
              .unwrap()
              .rune
              .unwrap()
              .commitment()
              .as_slice()
              .try_into()
              .unwrap(),
          )
          .into_script();
        let mut witness = Witness::default();
        witness.push(tapscript);
        witness.push([]);
        witness
      });

      let txid = self.core.broadcast_tx(TransactionTemplate {
        inputs: &[(block_count + 1, 1, 0, witness)],
        op_return: Some(runestone.encipher()),
        outputs,
        ..default()
      });

      self.mine_blocks(1);

      (
        txid,
        RuneId {
          block: (self.index.block_count().unwrap() - 1).into(),
          tx: 1,
        },
      )
    }

    #[track_caller]
    fn get(&self, path: impl AsRef<str>) -> reqwest::blocking::Response {
      if let Err(error) = self.index.update() {
        log::error!("{error}");
      }
      reqwest::blocking::get(self.join_url(path.as_ref())).unwrap()
    }

    #[track_caller]
    pub(crate) fn get_json<T: DeserializeOwned>(&self, path: impl AsRef<str>) -> T {
      if let Err(error) = self.index.update() {
        log::error!("{error}");
      }

      let client = reqwest::blocking::Client::new();

      let response = client
        .get(self.join_url(path.as_ref()))
        .header(header::ACCEPT, "application/json")
        .send()
        .unwrap();

      assert_eq!(response.status(), StatusCode::OK);

      response.json().unwrap()
    }

    #[track_caller]
    fn post_json<T: DeserializeOwned>(&self, path: impl AsRef<str>, body: &impl Serialize) -> T {
      if let Err(error) = self.index.update() {
        log::error!("{error}");
      }

      let client = reqwest::blocking::Client::new();

      let response = client
        .post(self.join_url(path.as_ref()))
        .json(body)
        .header(header::ACCEPT, "application/json")
        .send()
        .unwrap();

      assert_eq!(response.status(), StatusCode::OK);

      response.json().unwrap()
    }

    #[track_caller]
    fn post(
      &self,
      path: impl AsRef<str>,
      body: &str,
      status: StatusCode,
    ) -> reqwest::blocking::Response {
      if let Err(error) = self.index.update() {
        log::error!("{error}");
      }

      let client = reqwest::blocking::Client::new();

      let response = client
        .post(self.join_url(path.as_ref()))
        .body(body.as_bytes().to_vec())
        .send()
        .unwrap();

      assert_eq!(response.status(), status, "{}", response.text().unwrap());

      response
    }

    fn join_url(&self, url: &str) -> Url {
      self.url.join(url).unwrap()
    }

    #[track_caller]
    fn assert_response(&self, path: impl AsRef<str>, status: StatusCode, expected_response: &str) {
      let response = self.get(path);
      assert_eq!(response.status(), status, "{}", response.text().unwrap());
      pretty_assert_eq!(response.text().unwrap(), expected_response);
    }

    #[track_caller]
    fn assert_response_regex(
      &self,
      path: impl AsRef<str>,
      status: StatusCode,
      regex: impl AsRef<str>,
    ) {
      let response = self.get(path);
      assert_eq!(
        response.status(),
        status,
        "response: {}",
        response.text().unwrap()
      );
      assert_regex_match!(response.text().unwrap(), regex.as_ref());
    }

    #[track_caller]
    fn assert_html(&self, path: impl AsRef<str>, content: impl PageContent) {
      self.assert_html_status(path, StatusCode::OK, content);
    }

    #[track_caller]
    fn assert_html_status(
      &self,
      path: impl AsRef<str>,
      status: StatusCode,
      content: impl PageContent,
    ) {
      let response = self.get(path);

      assert_eq!(response.status(), status, "{}", response.text().unwrap());

      let expected_response = PageHtml::new(
        content,
        Arc::new(ServerConfig {
          chain: self.index.chain(),
          domain: Some(System::host_name().unwrap()),
          ..Default::default()
        }),
      )
      .to_string();

      pretty_assert_eq!(response.text().unwrap(), expected_response);
    }

    fn assert_response_csp(
      &self,
      path: impl AsRef<str>,
      status: StatusCode,
      content_security_policy: &str,
      regex: impl AsRef<str>,
    ) {
      let response = self.get(path);
      assert_eq!(response.status(), status);
      assert_eq!(
        response
          .headers()
          .get(header::CONTENT_SECURITY_POLICY,)
          .unwrap(),
        content_security_policy
      );
      assert_regex_match!(response.text().unwrap(), regex.as_ref());
    }

    #[track_caller]
    fn assert_redirect(&self, path: &str, location: &str) {
      let response = reqwest::blocking::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .build()
        .unwrap()
        .get(self.join_url(path))
        .send()
        .unwrap();

      assert_eq!(response.status(), StatusCode::SEE_OTHER);
      assert_eq!(response.headers().get(header::LOCATION).unwrap(), location);
    }

    #[track_caller]
    fn mine_blocks(&self, n: u64) -> Vec<Block> {
      let blocks = self.core.mine_blocks(n);
      self.index.update().unwrap();
      blocks
    }

    fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec<Block> {
      let blocks = self.core.mine_blocks_with_subsidy(n, subsidy);
      self.index.update().unwrap();
      blocks
    }
  }

  impl Drop for TestServer {
    fn drop(&mut self) {
      self.ord_server_handle.shutdown();
    }
  }

  fn parse_server_args(args: &str) -> (Settings, Server) {
    match Arguments::try_parse_from(args.split_whitespace()) {
      Ok(arguments) => match arguments.subcommand {
        Subcommand::Server(server) => (
          Settings::from_options(arguments.options)
            .or_defaults()
            .unwrap(),
          server,
        ),
        subcommand => panic!("unexpected subcommand: {subcommand:?}"),
      },
      Err(err) => panic!("error parsing arguments: {err}"),
    }
  }

  #[test]
  fn http_and_https_port_dont_conflict() {
    parse_server_args(
      "ord server --http-port 0 --https-port 0 --acme-cache foo --acme-contact bar --acme-domain baz",
    );
  }

  #[test]
  fn http_port_defaults_to_80() {
    assert_eq!(parse_server_args("ord server").1.http_port(), Some(80));
  }

  #[test]
  fn https_port_defaults_to_none() {
    assert_eq!(parse_server_args("ord server").1.https_port(), None);
  }

  #[test]
  fn https_sets_https_port_to_443() {
    assert_eq!(
      parse_server_args("ord server --https --acme-cache foo --acme-contact bar --acme-domain baz")
        .1
        .https_port(),
      Some(443)
    );
  }

  #[test]
  fn https_disables_http() {
    assert_eq!(
      parse_server_args("ord server --https --acme-cache foo --acme-contact bar --acme-domain baz")
        .1
        .http_port(),
      None
    );
  }

  #[test]
  fn https_port_disables_http() {
    assert_eq!(
      parse_server_args(
        "ord server --https-port 433 --acme-cache foo --acme-contact bar --acme-domain baz"
      )
      .1
      .http_port(),
      None
    );
  }

  #[test]
  fn https_port_sets_https_port() {
    assert_eq!(
      parse_server_args(
        "ord server --https-port 1000 --acme-cache foo --acme-contact bar --acme-domain baz"
      )
      .1
      .https_port(),
      Some(1000)
    );
  }

  #[test]
  fn http_with_https_leaves_http_enabled() {
    assert_eq!(
      parse_server_args(
        "ord server --https --http --acme-cache foo --acme-contact bar --acme-domain baz"
      )
      .1
      .http_port(),
      Some(80)
    );
  }

  #[test]
  fn http_with_https_leaves_https_enabled() {
    assert_eq!(
      parse_server_args(
        "ord server --https --http --acme-cache foo --acme-contact bar --acme-domain baz"
      )
      .1
      .https_port(),
      Some(443)
    );
  }

  #[test]
  fn acme_contact_accepts_multiple_values() {
    assert!(
      Arguments::try_parse_from([
        "ord",
        "server",
        "--address",
        "127.0.0.1",
        "--http-port",
        "0",
        "--acme-contact",
        "foo",
        "--acme-contact",
        "bar"
      ])
      .is_ok()
    );
  }

  #[test]
  fn acme_domain_accepts_multiple_values() {
    assert!(
      Arguments::try_parse_from([
        "ord",
        "server",
        "--address",
        "127.0.0.1",
        "--http-port",
        "0",
        "--acme-domain",
        "foo",
        "--acme-domain",
        "bar"
      ])
      .is_ok()
    );
  }

  #[test]
  fn acme_cache_defaults_to_data_dir() {
    let arguments = Arguments::try_parse_from(["ord", "--datadir", "foo", "server"]).unwrap();

    let settings = Settings::from_options(arguments.options)
      .or_defaults()
      .unwrap();

    let acme_cache = Server::acme_cache(None, &settings).display().to_string();
    assert!(
      acme_cache.contains(if cfg!(windows) {
        r"foo\acme-cache"
      } else {
        "foo/acme-cache"
      }),
      "{acme_cache}"
    )
  }

  #[test]
  fn acme_cache_flag_is_respected() {
    let arguments =
      Arguments::try_parse_from(["ord", "--datadir", "foo", "server", "--acme-cache", "bar"])
        .unwrap();

    let settings = Settings::from_options(arguments.options)
      .or_defaults()
      .unwrap();

    let acme_cache = Server::acme_cache(Some(&"bar".into()), &settings)
      .display()
      .to_string();
    assert_eq!(acme_cache, "bar")
  }

  #[test]
  fn acme_domain_defaults_to_hostname() {
    let (_, server) = parse_server_args("ord server");
    assert_eq!(
      server.acme_domains().unwrap(),
      &[System::host_name().unwrap()]
    );
  }

  #[test]
  fn acme_domain_flag_is_respected() {
    let (_, server) = parse_server_args("ord server --acme-domain example.com");
    assert_eq!(server.acme_domains().unwrap(), &["example.com"]);
  }

  #[test]
  fn install_sh_redirects_to_github() {
    TestServer::new().assert_redirect(
      "/install.sh",
      "https://raw.githubusercontent.com/ordinals/ord/master/install.sh",
    );
  }

  #[test]
  fn ordinal_redirects_to_sat() {
    TestServer::new().assert_redirect("/ordinal/0", "/sat/0");
  }

  #[test]
  fn bounties_redirects_to_docs_site() {
    TestServer::new().assert_redirect("/bounties", "https://docs.ordinals.com/bounties");
  }

  #[test]
  fn faq_redirects_to_docs_site() {
    TestServer::new().assert_redirect("/faq", "https://docs.ordinals.com/faq");
  }

  #[test]
  fn search_by_query_returns_rune() {
    TestServer::new().assert_redirect("/search?query=ABCD", "/rune/ABCD");
  }

  #[test]
  fn search_by_query_returns_spaced_rune() {
    TestServer::new().assert_redirect("/search?query=AB•CD", "/rune/AB•CD");
  }

  #[test]
  fn search_by_query_returns_satscard() {
    TestServer::new().assert_redirect(
      "/search?query=https://satscard.com/start%23foo",
      "/satscard?foo",
    );
    TestServer::new().assert_redirect(
      "/search?query=https://getsatscard.com/start%23foo",
      "/satscard?foo",
    );
    TestServer::new().assert_redirect(
      "/search?query=https://ordinals.com/satscard?foo",
      "/satscard?foo",
    );
  }

  #[test]
  fn search_by_query_returns_inscription() {
    TestServer::new().assert_redirect(
      "/search?query=0000000000000000000000000000000000000000000000000000000000000000i0",
      "/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
    );
  }

  #[test]
  fn search_by_query_returns_inscription_by_number() {
    TestServer::new().assert_redirect("/search?query=0", "/inscription/0");
  }

  #[test]
  fn search_is_whitespace_insensitive() {
    TestServer::new().assert_redirect("/search/ abc ", "/sat/abc");
  }

  #[test]
  fn search_by_path_returns_sat() {
    TestServer::new().assert_redirect("/search/abc", "/sat/abc");
  }

  #[test]
  fn search_for_blockhash_returns_block() {
    TestServer::new().assert_redirect(
      "/search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
      "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
    );
  }

  #[test]
  fn search_for_txid_returns_transaction() {
    TestServer::new().assert_redirect(
      "/search/0000000000000000000000000000000000000000000000000000000000000000",
      "/tx/0000000000000000000000000000000000000000000000000000000000000000",
    );
  }

  #[test]
  fn search_for_outpoint_returns_output() {
    TestServer::new().assert_redirect(
      "/search/0000000000000000000000000000000000000000000000000000000000000000:0",
      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
    );
  }

  #[test]
  fn search_for_inscription_id_returns_inscription() {
    TestServer::new().assert_redirect(
      "/search/0000000000000000000000000000000000000000000000000000000000000000i0",
      "/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
    );
  }

  #[test]
  fn search_by_path_returns_rune() {
    TestServer::new().assert_redirect("/search/ABCD", "/rune/ABCD");
  }

  #[test]
  fn search_by_path_returns_spaced_rune() {
    TestServer::new().assert_redirect("/search/AB•CD", "/rune/AB•CD");
  }

  #[test]
  fn search_by_rune_id_returns_rune() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");

    server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    server.mine_blocks(1);

    server.assert_redirect("/search/8:1", "/rune/AAAAAAAAAAAAA");
    server.assert_redirect("/search?query=8:1", "/rune/AAAAAAAAAAAAA");

    server.assert_response_regex(
      "/search/100000000000000000000:200000000000000000",
      StatusCode::BAD_REQUEST,
      ".*",
    );
  }

  #[test]
  fn search_by_satpoint_returns_sat() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

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

    server.assert_redirect(
      &format!("/search/{txid}:0:0"),
      &format!("/satpoint/{txid}:0:0"),
    );

    server.assert_redirect(
      &format!("/search?query={txid}:0:0"),
      &format!("/satpoint/{txid}:0:0"),
    );

    server.assert_redirect(
      &format!("/satpoint/{txid}:0:0"),
      &format!("/sat/{}", 50 * COIN_VALUE),
    );

    server.assert_response_regex("/search/1:2:3", StatusCode::BAD_REQUEST, ".*");
  }

  #[test]
  fn satpoint_returns_sat_in_multiple_ranges() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    let split = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      outputs: 2,
      fee: 0,
      ..default()
    };

    server.core.broadcast_tx(split);

    server.mine_blocks(1);

    let merge = TransactionTemplate {
      inputs: &[(2, 0, 0, Default::default()), (2, 1, 0, Default::default())],
      fee: 0,
      ..default()
    };

    let txid = server.core.broadcast_tx(merge);

    server.mine_blocks(1);

    server.assert_redirect(
      &format!("/satpoint/{txid}:0:0"),
      &format!("/sat/{}", 100 * COIN_VALUE),
    );

    server.assert_redirect(
      &format!("/satpoint/{txid}:0:{}", 50 * COIN_VALUE),
      &format!("/sat/{}", 50 * COIN_VALUE),
    );

    server.assert_redirect(
      &format!("/satpoint/{txid}:0:{}", 50 * COIN_VALUE - 1),
      &format!("/sat/{}", 150 * COIN_VALUE - 1),
    );
  }

  #[test]
  fn fallback() {
    let server = TestServer::new();

    server.assert_redirect("/0", "/inscription/0");
    server.assert_redirect("/0/", "/inscription/0");
    server.assert_redirect("/0//", "/inscription/0");
    server.assert_redirect(
      "/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0",
      "/inscription/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0",
    );
    server.assert_redirect("/-1", "/inscription/-1");
    server.assert_redirect("/FOO", "/rune/FOO");
    server.assert_redirect("/FO.O", "/rune/FO.O");
    server.assert_redirect("/FO•O", "/rune/FO•O");
    server.assert_redirect("/0:0", "/rune/0:0");
    server.assert_redirect(
      "/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
      "/output/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
    );
    server.assert_redirect(
      "/4273262611454246626680278280877079635139930168289368354696278617:0",
      "/output/4273262611454246626680278280877079635139930168289368354696278617:0",
    );
    server.assert_redirect(
      "/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0",
      "/satpoint/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0",
    );
    server.assert_redirect(
      "/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
      "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
    );
    server.assert_redirect(
      "/000000000000000000000000000000000000000000000000000000000000000f",
      "/tx/000000000000000000000000000000000000000000000000000000000000000f",
    );
    server.assert_redirect(
      "/4273262611454246626680278280877079635139930168289368354696278617",
      "/tx/4273262611454246626680278280877079635139930168289368354696278617",
    );
    server.assert_redirect(
      "/bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297",
      "/address/bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297",
    );
    server.assert_redirect(
      "/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
      "/address/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
    );
    server.assert_redirect(
      "/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
      "/address/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
    );

    server.assert_response_regex("/hello", StatusCode::NOT_FOUND, "");

    server.assert_response_regex(
      "/%C3%28",
      StatusCode::BAD_REQUEST,
      "invalid utf-8 sequence of 1 bytes from index 0",
    );
  }

  #[test]
  fn runes_can_be_queried_by_rune_id() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex("/rune/9:1", StatusCode::NOT_FOUND, ".*");

    server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    server.mine_blocks(1);

    server.assert_response_regex(
      "/rune/8:1",
      StatusCode::OK,
      ".*<title>Rune AAAAAAAAAAAAA</title>.*",
    );
  }

  #[test]
  fn runes_can_be_queried_by_rune_number() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_response_regex("/rune/0", StatusCode::NOT_FOUND, ".*");

    for i in 0..10 {
      let rune = Rune(RUNE + i);
      server.etch(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching {
            rune: Some(rune),
            ..default()
          }),
          ..default()
        },
        1,
        None,
      );

      server.mine_blocks(1);
    }

    server.assert_response_regex(
      "/rune/0",
      StatusCode::OK,
      ".*<title>Rune AAAAAAAAAAAAA</title>.*",
    );

    for i in 1..6 {
      server.assert_response_regex(
        format!("/rune/{i}"),
        StatusCode::OK,
        ".*<title>Rune AAAAAAAAAAAA.*</title>.*",
      );
    }

    server.assert_response_regex(
      "/rune/9",
      StatusCode::OK,
      ".*<title>Rune AAAAAAAAAAAAJ</title>.*",
    );
  }

  #[test]
  fn rune_not_etched_shows_unlock_height() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_html_status(
      "/rune/A",
      StatusCode::NOT_FOUND,
      RuneNotFoundHtml {
        rune: Rune(0),
        unlock: Some((
          Height(209999),
          Blocktime::Expected(DateTime::from_timestamp(125998800, 0).unwrap()),
        )),
      },
    );
  }

  #[test]
  fn reserved_rune_not_etched_shows_reserved_status() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_html_status(
      format!("/rune/{}", Rune(Rune::RESERVED)),
      StatusCode::NOT_FOUND,
      RuneNotFoundHtml {
        rune: Rune(Rune::RESERVED),
        unlock: None,
      },
    );
  }

  #[test]
  fn runes_are_displayed_on_runes_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_html(
      "/runes",
      RunesHtml {
        entries: Vec::new(),
        more: false,
        prev: None,
        next: None,
      },
    );

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          symbol: Some('%'),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      Default::default(),
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0
          },
          premine: u128::MAX,
          timestamp: id.block,
          symbol: Some('%'),
          ..default()
        }
      )]
    );

    assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
    );

    server.assert_html(
      "/runes",
      RunesHtml {
        entries: vec![(
          RuneId::default(),
          RuneEntry {
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            ..default()
          },
        )],
        more: false,
        prev: None,
        next: None,
      },
    );
  }

  #[test]
  fn runes_are_displayed_on_rune_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune),
          symbol: Some('%'),
          premine: Some(u128::MAX),
          turbo: true,
          ..default()
        }),
        ..default()
      },
      1,
      Some(
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("hello".into()),
          rune: Some(rune.commitment()),
          ..default()
        }
        .to_witness(),
      ),
    );

    let entry = RuneEntry {
      block: id.block,
      etching: txid,
      spaced_rune: SpacedRune { rune, spacers: 0 },
      premine: u128::MAX,
      symbol: Some('%'),
      timestamp: id.block,
      turbo: true,
      ..default()
    };

    assert_eq!(server.index.runes().unwrap(), [(id, entry)]);

    assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
    );

    let parent = InscriptionId { txid, index: 0 };

    server.assert_html(
      format!("/rune/{rune}"),
      RuneHtml {
        id,
        entry,
        mintable: false,
        parent: Some(parent),
      },
    );

    server.assert_response_regex(
      format!("/inscription/{parent}"),
      StatusCode::OK,
      ".*
<dl>
  <dt>rune</dt>
  <dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>
  .*
</dl>
.*",
    );
  }

  #[test]
  fn etched_runes_are_displayed_on_block_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune0 = Rune(RUNE);

    let (_txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune0),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    assert_eq!(
      server.index.get_runes_in_block(id.block - 1).unwrap().len(),
      0
    );
    assert_eq!(server.index.get_runes_in_block(id.block).unwrap().len(), 1);
    assert_eq!(
      server.index.get_runes_in_block(id.block + 1).unwrap().len(),
      0
    );

    server.assert_response_regex(
      format!("/block/{}", id.block),
      StatusCode::OK,
      format!(".*<h2>1 Rune</h2>.*<li><a href=/rune/{rune0}>{rune0}</a></li>.*"),
    );
  }

  #[test]
  fn runes_are_spaced() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune),
          symbol: Some('%'),
          spacers: Some(1),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      Some(
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("hello".into()),
          rune: Some(rune.commitment()),
          ..default()
        }
        .to_witness(),
      ),
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune { rune, spacers: 1 },
          premine: u128::MAX,
          symbol: Some('%'),
          timestamp: id.block,
          ..default()
        }
      )]
    );

    assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
    );

    server.assert_response_regex(
      format!("/rune/{rune}"),
      StatusCode::OK,
      r".*<title>Rune A•AAAAAAAAAAAA</title>.*<h1>A•AAAAAAAAAAAA</h1>.*",
    );

    server.assert_response_regex(
      format!("/inscription/{txid}i0"),
      StatusCode::OK,
      ".*<dt>rune</dt>.*<dd><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></dd>.*",
    );

    server.assert_response_regex(
      "/runes",
      StatusCode::OK,
      ".*<li><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></li>.*",
    );

    server.assert_response_regex(
      format!("/tx/{txid}"),
      StatusCode::OK,
      ".*
  <dt>etching</dt>
  <dd><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></dd>
.*",
    );

    server.assert_response_regex(
      format!("/output/{txid}:0"),
      StatusCode::OK,
      ".*<tr>
        <td><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></td>
        <td>340282366920938463463374607431768211455\u{A0}%</td>
      </tr>.*",
    );
  }

  #[test]
  fn transactions_link_to_etching() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_response_regex(
      "/runes",
      StatusCode::OK,
      ".*<title>Runes</title>.*<h1>Runes</h1>\n<ul>\n</ul>.*",
    );

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        }
      )]
    );

    pretty_assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
    );

    server.assert_response_regex(
      format!("/tx/{txid}"),
      StatusCode::OK,
      ".*
  <dt>etching</dt>
  <dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>
.*",
    );
  }

  #[test]
  fn runes_are_displayed_on_output_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          divisibility: Some(1),
          rune: Some(rune),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          divisibility: 1,
          etching: txid,
          spaced_rune: SpacedRune { rune, spacers: 0 },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        }
      )]
    );

    let output = OutPoint { txid, vout: 0 };

    assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(output, vec![(id, u128::MAX)])]
    );

    server.assert_response_regex(
      format!("/output/{output}"),
      StatusCode::OK,
      format!(
        ".*<title>Output {output}</title>.*<h1>Output <span class=monospace>{output}</span></h1>.*
  <dt>runes</dt>
  <dd>
    <table>
      <tr>
        <th>rune</th>
        <th>balance</th>
      </tr>
      <tr>
        <td><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></td>
        <td>34028236692093846346337460743176821145.5\u{A0}¤</td>
      </tr>
    </table>
  </dd>
.*"
      ),
    );

    let address = default_address(Chain::Regtest);

    pretty_assert_eq!(
      server.get_json::<api::Output>(format!("/output/{output}")),
      api::Output {
        value: 5000000000,
        script_pubkey: address.script_pubkey(),
        address: Some(uncheck(&address)),
        confirmations: 1,
        transaction: txid,
        sat_ranges: None,
        indexed: true,
        inscriptions: Some(Vec::new()),
        outpoint: output,
        runes: Some(
          vec![(
            SpacedRune {
              rune: Rune(RUNE),
              spacers: 0
            },
            Pile {
              amount: 340282366920938463463374607431768211455,
              divisibility: 1,
              symbol: None,
            }
          )]
          .into_iter()
          .collect()
        ),
        spent: false,
      }
    );
  }

  #[test]
  fn http_to_https_redirect_with_path() {
    TestServer::builder()
      .redirect_http_to_https()
      .https()
      .build()
      .assert_redirect(
        "/sat/0",
        &format!("https://{}/sat/0", System::host_name().unwrap()),
      );
  }

  #[test]
  fn http_to_https_redirect_with_empty() {
    TestServer::builder()
      .redirect_http_to_https()
      .https()
      .build()
      .assert_redirect("/", &format!("https://{}/", System::host_name().unwrap()));
  }

  #[test]
  fn status() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(3);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/plain;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        2,
        0,
        0,
        inscription("text/plain;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        3,
        0,
        0,
        Inscription {
          content_type: None,
          body: Some("hello".as_bytes().into()),
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/status",
      StatusCode::OK,
      ".*<h1>Status</h1>
<dl>
  <dt>chain</dt>
  <dd>regtest</dd>
  <dt>height</dt>
  <dd><a href=/block/4>4</a></dd>
  <dt>inscriptions</dt>
  <dd><a href=/inscriptions>3</a></dd>
  <dt>blessed inscriptions</dt>
  <dd>3</dd>
  <dt>cursed inscriptions</dt>
  <dd>0</dd>
  <dt>runes</dt>
  <dd><a href=/runes>0</a></dd>
  <dt>lost sats</dt>
  <dd>.*</dd>
  <dt>started</dt>
  <dd>.*</dd>
  <dt>uptime</dt>
  <dd>.*</dd>
  <dt>minimum rune for next block</dt>
  <dd>.*</dd>
  <dt>version</dt>
  <dd>.*</dd>
  <dt>unrecoverably reorged</dt>
  <dd>false</dd>
  <dt>address index</dt>
  <dd>false</dd>
  <dt>inscription index</dt>
  <dd>true</dd>
  <dt>rune index</dt>
  <dd>false</dd>
  <dt>sat index</dt>
  <dd>false</dd>
  <dt>transaction index</dt>
  <dd>false</dd>
  <dt>json api</dt>
  <dd>true</dd>
  <dt>git branch</dt>
  <dd>.*</dd>
  <dt>git commit</dt>
  <dd>
    <a class=collapse href=https://github.com/ordinals/ord/commit/[[:xdigit:]]{40}>
      [[:xdigit:]]{40}
    </a>
  </dd>
</dl>
.*",
    );
  }

  #[test]
  fn block_count_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blockcount");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "1");

    test_server.mine_blocks(1);

    let response = test_server.get("/blockcount");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "2");
  }

  #[test]
  fn block_height_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blockheight");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "0");

    test_server.mine_blocks(2);

    let response = test_server.get("/blockheight");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "2");
  }

  #[test]
  fn block_hash_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blockhash");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
      response.text().unwrap(),
      "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
    );
  }

  #[test]
  fn block_hash_from_height_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blockhash/0");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
      response.text().unwrap(),
      "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
    );
  }

  #[test]
  fn block_time_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blocktime");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "1231006505");
  }

  #[test]
  fn sat_number() {
    TestServer::new().assert_response_regex("/sat/0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
  }

  #[test]
  fn sat_decimal() {
    TestServer::new().assert_response_regex("/sat/0.0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
  }

  #[test]
  fn sat_degree() {
    TestServer::new().assert_response_regex("/sat/0°0′0″0‴", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
  }

  #[test]
  fn sat_name() {
    TestServer::new().assert_response_regex(
      "/sat/nvtdijuwxlp",
      StatusCode::OK,
      ".*<h1>Sat 0</h1>.*",
    );
  }

  #[test]
  fn sat() {
    TestServer::new().assert_response_regex(
      "/sat/0",
      StatusCode::OK,
      ".*<title>Sat 0</title>.*<h1>Sat 0</h1>.*",
    );
  }

  #[test]
  fn block() {
    TestServer::new().assert_response_regex(
      "/block/0",
      StatusCode::OK,
      ".*<title>Block 0</title>.*<h1>Block 0</h1>.*",
    );
  }

  #[test]
  fn sat_out_of_range() {
    TestServer::new().assert_response(
      "/sat/2099999997690000",
      StatusCode::BAD_REQUEST,
      "Invalid URL: failed to parse sat `2099999997690000`: invalid integer range",
    );
  }

  #[test]
  fn invalid_outpoint_hash_returns_400() {
    TestServer::new().assert_response(
      "/output/foo:0",
      StatusCode::BAD_REQUEST,
      "Invalid URL: Cannot parse `output` with value `foo:0`: error parsing TXID",
    );
  }

  #[test]
  fn output_with_sat_index() {
    let txid = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
    TestServer::builder()
      .index_sats()
      .build()
      .assert_response_regex(
        format!("/output/{txid}:0"),
        StatusCode::OK,
        format!(
          ".*<title>Output {txid}:0</title>.*<h1>Output <span class=monospace>{txid}:0</span></h1>
<dl>
  <dt>value</dt><dd>5000000000</dd>
  <dt>script pubkey</dt><dd class=monospace>OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG</dd>
  <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
  <dt>confirmations</dt><dd>1</dd>
  <dt>spent</dt><dd>false</dd>
</dl>
<h2>1 Sat Range</h2>
<ul class=monospace>
  <li><a href=/sat/0 class=mythic>0</a>-<a href=/sat/4999999999 class=common>4999999999</a> \\(5000000000 sats\\)</li>
</ul>.*"
        ),
      );
  }

  #[test]
  fn output_without_sat_index() {
    let txid = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
    TestServer::new().assert_response_regex(
      format!("/output/{txid}:0"),
      StatusCode::OK,
      format!(
        ".*<title>Output {txid}:0</title>.*<h1>Output <span class=monospace>{txid}:0</span></h1>
<dl>
  <dt>value</dt><dd>5000000000</dd>
  <dt>script pubkey</dt><dd class=monospace>OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG</dd>
  <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
  <dt>confirmations</dt><dd>1</dd>
  <dt>spent</dt><dd>false</dd>
</dl>.*"
      ),
    );
  }

  #[test]
  fn null_output_receives_lost_sats() {
    let server = TestServer::builder().index_sats().build();

    server.mine_blocks_with_subsidy(1, 0);

    let txid = "0000000000000000000000000000000000000000000000000000000000000000";

    server.assert_response_regex(
      format!("/output/{txid}:4294967295"),
      StatusCode::OK,
      format!(
        ".*<title>Output {txid}:4294967295</title>.*<h1>Output <span class=monospace>{txid}:4294967295</span></h1>
<dl>
  <dt>value</dt><dd>5000000000</dd>
  <dt>script pubkey</dt><dd class=monospace></dd>
  <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
  <dt>confirmations</dt><dd>0</dd>
  <dt>spent</dt><dd>false</dd>
</dl>
<h2>1 Sat Range</h2>
<ul class=monospace>
  <li><a href=/sat/5000000000 class=uncommon>5000000000</a>-<a href=/sat/9999999999 class=common>9999999999</a> \\(5000000000 sats\\)</li>
</ul>.*"
      ),
    );
  }

  #[test]
  fn unbound_output_receives_unbound_inscriptions() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 50 * 100_000_000,
      ..default()
    });

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        2,
        1,
        0,
        inscription("text/plain;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{inscription_id}"),
      StatusCode::OK,
      format!(
        ".*<dl>
  <dt>id</dt>
  <dd class=collapse>{inscription_id}</dd>.*<dt>output</dt>
  <dd><a class=collapse href=/output/0000000000000000000000000000000000000000000000000000000000000000:0>0000000000000000000000000000000000000000000000000000000000000000:0</a></dd>.*"
      ),
    );

    server.assert_response_regex(
      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
      StatusCode::OK,
      ".*<h1>Output <span class=monospace>0000000000000000000000000000000000000000000000000000000000000000:0</span></h1>
<dl>
  <dt>inscriptions</dt>
  <dd class=thumbnails>
    <a href=/inscription/.*><iframe sandbox=allow-scripts scrolling=no loading=lazy src=/preview/.*></iframe></a>
  </dd>.*",
    );
  }

  #[test]
  fn unbound_output_returns_200() {
    TestServer::new().assert_response_regex(
      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
      StatusCode::OK,
      ".*",
    );
  }

  #[test]
  fn invalid_output_returns_400() {
    TestServer::new().assert_response(
      "/output/foo:0",
      StatusCode::BAD_REQUEST,
      "Invalid URL: Cannot parse `output` with value `foo:0`: error parsing TXID",
    );
  }

  #[test]
  fn home() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let mut ids = Vec::new();

    for i in 0..101 {
      let txid = server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 0, 0, inscription("image/png", "hello").to_witness())],
        ..default()
      });
      ids.push(InscriptionId { txid, index: 0 });
      server.mine_blocks(1);
    }

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(102, 0, 0, inscription("text/plain", "{}").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/",
      StatusCode::OK,
      format!(
        r".*<title>Ordinals</title>.*
<h1>Latest Inscriptions</h1>
<div class=thumbnails>
  <a href=/inscription/{}>.*</a>
  (<a href=/inscription/[[:xdigit:]]{{64}}i0>.*</a>\s*){{99}}
</div>
.*
",
        ids[100]
      ),
    );
  }

  #[test]
  fn blocks() {
    let test_server = TestServer::new();

    test_server.mine_blocks(1);

    test_server.assert_response_regex(
      "/blocks",
      StatusCode::OK,
      ".*<title>Blocks</title>.*
<h1>Blocks</h1>
<div class=block>
  <h2><a href=/block/1>Block 1</a></h2>
  <div class=thumbnails>
  </div>
</div>
<div class=block>
  <h2><a href=/block/0>Block 0</a></h2>
  <div class=thumbnails>
  </div>
</div>
</ol>.*",
    );
  }

  #[test]
  fn nav_displays_chain() {
    TestServer::builder()
      .chain(Chain::Regtest)
      .build()
      .assert_response_regex(
        "/",
        StatusCode::OK,
        ".*<a href=/ title=home>Ordinals<sup>regtest</sup></a>.*",
      );
  }

  #[test]
  fn blocks_block_limit() {
    let test_server = TestServer::new();

    test_server.mine_blocks(101);

    test_server.assert_response_regex(
      "/blocks",
      StatusCode::OK,
      ".*<ol start=96 reversed class=block-list>\n(  <li><a class=collapse href=/block/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>\n){95}</ol>.*"
    );
  }

  #[test]
  fn block_not_found() {
    TestServer::new().assert_response(
      "/block/467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16",
      StatusCode::NOT_FOUND,
      "block 467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16 not found",
    );
  }

  #[test]
  fn unmined_sat() {
    TestServer::new().assert_response_regex(
      "/sat/0",
      StatusCode::OK,
      ".*<dt>timestamp</dt><dd><time>2009-01-03 18:15:05 UTC</time></dd>.*",
    );
  }

  #[test]
  fn mined_sat() {
    TestServer::new().assert_response_regex(
      "/sat/5000000000",
      StatusCode::OK,
      ".*<dt>timestamp</dt><dd><time>.*</time> \\(expected\\)</dd>.*",
    );
  }

  #[test]
  fn static_asset() {
    TestServer::new().assert_response_regex(
      "/static/index.css",
      StatusCode::OK,
      r".*\.rare \{
  background-color: var\(--rare\);
}.*",
    );
  }

  #[test]
  fn favicon() {
    TestServer::new().assert_response_regex("/favicon.ico", StatusCode::OK, r".*");
  }

  #[test]
  fn clock_updates() {
    let test_server = TestServer::new();
    test_server.assert_response_regex("/clock", StatusCode::OK, ".*<text.*>0</text>.*");
    test_server.mine_blocks(1);
    test_server.assert_response_regex("/clock", StatusCode::OK, ".*<text.*>1</text>.*");
  }

  #[test]
  fn block_by_hash() {
    let test_server = TestServer::new();

    test_server.mine_blocks(1);
    let transaction = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 0,
      ..default()
    };
    test_server.core.broadcast_tx(transaction);
    let block_hash = test_server.mine_blocks(1)[0].block_hash();

    test_server.assert_response_regex(
      format!("/block/{block_hash}"),
      StatusCode::OK,
      ".*<h1>Block 2</h1>.*",
    );
  }

  #[test]
  fn block_by_height() {
    let test_server = TestServer::new();

    test_server.assert_response_regex("/block/0", StatusCode::OK, ".*<h1>Block 0</h1>.*");
  }

  #[test]
  fn transaction() {
    let test_server = TestServer::new();

    let coinbase_tx = test_server.mine_blocks(1)[0].txdata[0].clone();
    let txid = coinbase_tx.compute_txid();

    test_server.assert_response_regex(
      format!("/tx/{txid}"),
      StatusCode::OK,
      format!(
        ".*<title>Transaction {txid}</title>.*<h1>Transaction <span class=monospace>{txid}</span></h1>
<dl>
</dl>
<h2>1 Input</h2>
<ul>
  <li><a class=collapse href=/output/0000000000000000000000000000000000000000000000000000000000000000:4294967295>0000000000000000000000000000000000000000000000000000000000000000:4294967295</a></li>
</ul>
<h2>1 Output</h2>
<ul class=monospace>
  <li>
    <a href=/output/{txid}:0 class=collapse>
      {txid}:0
    </a>
    <dl>
      <dt>value</dt><dd>5000000000</dd>
      <dt>script pubkey</dt><dd class=monospace>.*</dd>
    </dl>
  </li>
</ul>.*"
      ),
    );
  }

  #[test]
  fn recursive_transaction_hex_endpoint() {
    let test_server = TestServer::new();

    let coinbase_tx = test_server.mine_blocks(1)[0].txdata[0].clone();
    let txid = coinbase_tx.compute_txid();

    test_server.assert_response(
      format!("/r/tx/{txid}"),
      StatusCode::OK,
      "\"02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0151ffffffff0100f2052a01000000225120be7cbbe9ca06a7d7b2a17c6b4ff4b85b362cbcd7ee1970daa66dfaa834df59a000000000\""
    );
  }

  #[test]
  fn recursive_transaction_hex_endpoint_for_genesis_transaction() {
    let test_server = TestServer::new();

    test_server.mine_blocks(1);

    test_server.assert_response(
      "/r/tx/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
      StatusCode::OK,
      "\"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000\""
    );
  }

  #[test]
  fn detect_unrecoverable_reorg() {
    let test_server = TestServer::new();

    test_server.mine_blocks(21);

    test_server.assert_response_regex(
      "/status",
      StatusCode::OK,
      ".*<dt>unrecoverably reorged</dt>\n  <dd>false</dd>.*",
    );

    for _ in 0..15 {
      test_server.core.invalidate_tip();
    }

    test_server.core.mine_blocks(21);

    test_server.assert_response_regex(
      "/status",
      StatusCode::OK,
      ".*<dt>unrecoverably reorged</dt>\n  <dd>true</dd>.*",
    );
  }

  #[test]
  fn rare_with_sat_index() {
    TestServer::builder().index_sats().build().assert_response(
      "/rare.txt",
      StatusCode::OK,
      "sat\tsatpoint
0\t4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0
",
    );
  }

  #[test]
  fn rare_without_sat_index() {
    TestServer::new().assert_response(
      "/rare.txt",
      StatusCode::OK,
      "sat\tsatpoint
",
    );
  }

  #[test]
  fn show_rare_txt_in_header_with_sat_index() {
    TestServer::builder()
      .index_sats()
      .build()
      .assert_response_regex(
        "/",
        StatusCode::OK,
        ".*
      <a href=/clock title=clock>.*</a>
      <a href=/rare.txt title=rare>.*</a>.*",
      );
  }

  #[test]
  fn rare_sat_location() {
    TestServer::builder()
      .index_sats()
      .build()
      .assert_response_regex(
        "/sat/0",
        StatusCode::OK,
        ".*>4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0<.*",
      );
  }

  #[test]
  fn dont_show_rare_txt_in_header_without_sat_index() {
    TestServer::new().assert_response_regex(
      "/",
      StatusCode::OK,
      ".*
      <a href=/clock title=clock>.*</a>
      <a href=https://docs.ordinals.com/.*",
    );
  }

  #[test]
  fn input() {
    TestServer::new().assert_response_regex(
      "/input/0/0/0",
      StatusCode::OK,
      ".*<title>Input /0/0/0</title>.*<h1>Input /0/0/0</h1>.*<dt>text</dt><dd>.*The Times 03/Jan/2009 Chancellor on brink of second bailout for banks</dd>.*",
    );
  }

  #[test]
  fn input_missing() {
    TestServer::new().assert_response(
      "/input/1/1/1",
      StatusCode::NOT_FOUND,
      "input /1/1/1 not found",
    );
  }

  #[test]
  fn commits_are_tracked() {
    let server = TestServer::new();

    thread::sleep(Duration::from_millis(100));
    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1);

    let info = server.index.info().unwrap();
    assert_eq!(info.transactions.len(), 1);
    assert_eq!(info.transactions[0].starting_block_count, 0);

    server.index.update().unwrap();

    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1);

    let info = server.index.info().unwrap();
    assert_eq!(info.transactions.len(), 1);
    assert_eq!(info.transactions[0].starting_block_count, 0);

    server.mine_blocks(1);

    thread::sleep(Duration::from_millis(10));
    server.index.update().unwrap();

    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 2);

    let info = server.index.info().unwrap();
    assert_eq!(info.transactions.len(), 2);
    assert_eq!(info.transactions[0].starting_block_count, 0);
    assert_eq!(info.transactions[1].starting_block_count, 1);
    assert!(
      info.transactions[1].starting_timestamp - info.transactions[0].starting_timestamp >= 10
    );
  }

  #[test]
  fn outputs_traversed_are_tracked() {
    let server = TestServer::builder().index_sats().build();

    assert_eq!(
      server
        .index
        .statistic(crate::index::Statistic::OutputsTraversed),
      1
    );

    server.index.update().unwrap();

    assert_eq!(
      server
        .index
        .statistic(crate::index::Statistic::OutputsTraversed),
      1
    );

    server.mine_blocks(2);

    server.index.update().unwrap();

    assert_eq!(
      server
        .index
        .statistic(crate::index::Statistic::OutputsTraversed),
      3
    );
  }

  #[test]
  fn coinbase_sat_ranges_are_tracked() {
    let server = TestServer::builder().index_sats().build();

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      1
    );

    server.mine_blocks(1);

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      2
    );

    server.mine_blocks(1);

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      3
    );
  }

  #[test]
  fn split_sat_ranges_are_tracked() {
    let server = TestServer::builder().index_sats().build();

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      1
    );

    server.mine_blocks(1);
    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      outputs: 2,
      fee: 0,
      ..default()
    });
    server.mine_blocks(1);

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      4,
    );
  }

  #[test]
  fn fee_sat_ranges_are_tracked() {
    let server = TestServer::builder().index_sats().build();

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      1
    );

    server.mine_blocks(1);
    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      outputs: 2,
      fee: 2,
      ..default()
    });
    server.mine_blocks(1);

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      5,
    );
  }

  #[test]
  fn content_response_no_content() {
    assert_eq!(
      r::content_response(
        Inscription {
          content_type: Some("text/plain".as_bytes().to_vec()),
          body: None,
          ..default()
        },
        AcceptEncoding::default(),
        &ServerConfig::default(),
        true,
      )
      .unwrap(),
      None
    );
  }

  #[test]
  fn content_response_with_content() {
    let (headers, body) = r::content_response(
      Inscription {
        content_type: Some("text/plain".as_bytes().to_vec()),
        body: Some(vec![1, 2, 3]),
        ..default()
      },
      AcceptEncoding::default(),
      &ServerConfig::default(),
      true,
    )
    .unwrap()
    .unwrap();

    assert_eq!(headers["content-type"], "text/plain");
    assert_eq!(body, vec![1, 2, 3]);
  }

  #[test]
  fn content_security_policy_no_origin() {
    let (headers, _) = r::content_response(
      Inscription {
        content_type: Some("text/plain".as_bytes().to_vec()),
        body: Some(vec![1, 2, 3]),
        ..default()
      },
      AcceptEncoding::default(),
      &ServerConfig::default(),
      true,
    )
    .unwrap()
    .unwrap();

    assert_eq!(
      headers["content-security-policy"],
      HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:")
    );
  }

  #[test]
  fn content_security_policy_with_origin() {
    let (headers, _) = r::content_response(
      Inscription {
        content_type: Some("text/plain".as_bytes().to_vec()),
        body: Some(vec![1, 2, 3]),
        ..default()
      },
      AcceptEncoding::default(),
      &ServerConfig {
        csp_origin: Some("https://ordinals.com".into()),
        ..default()
      },
      true,
    )
    .unwrap()
    .unwrap();

    assert_eq!(
      headers["content-security-policy"],
      HeaderValue::from_static(
        "default-src https://ordinals.com/content/ https://ordinals.com/blockheight https://ordinals.com/blockhash https://ordinals.com/blockhash/ https://ordinals.com/blocktime https://ordinals.com/r/ 'unsafe-eval' 'unsafe-inline' data: blob:"
      )
    );
  }

  #[test]
  fn preview_content_security_policy() {
    {
      let server = TestServer::builder().chain(Chain::Regtest).build();

      server.mine_blocks(1);

      let txid = server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      server.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      server.assert_response_csp(
        format!("/preview/{inscription_id}"),
        StatusCode::OK,
        "default-src 'self'",
        format!(
          ".*<html lang=en data-inscription={inscription_id}>.*<title>Inscription 0 Preview</title>.*"
        ),
      );
    }

    {
      let server = TestServer::builder()
        .chain(Chain::Regtest)
        .server_option("--csp-origin", "https://ordinals.com")
        .build();

      server.mine_blocks(1);

      let txid = server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      server.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      server.assert_response_csp(
        format!("/preview/{inscription_id}"),
        StatusCode::OK,
        "default-src https://ordinals.com",
        format!(".*<html lang=en data-inscription={inscription_id}>.*"),
      );
    }
  }

  #[test]
  fn code_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/javascript", "hello").to_witness(),
      )],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<html lang=en data-inscription={inscription_id} data-language=javascript>.*"),
    );
  }

  #[test]
  fn content_response_no_content_type() {
    let (headers, body) = r::content_response(
      Inscription {
        content_type: None,
        body: Some(Vec::new()),
        ..default()
      },
      AcceptEncoding::default(),
      &ServerConfig::default(),
      true,
    )
    .unwrap()
    .unwrap();

    assert_eq!(headers["content-type"], "application/octet-stream");
    assert!(body.is_empty());
  }

  #[test]
  fn content_response_bad_content_type() {
    let (headers, body) = r::content_response(
      Inscription {
        content_type: Some("\n".as_bytes().to_vec()),
        body: Some(Vec::new()),
        ..Default::default()
      },
      AcceptEncoding::default(),
      &ServerConfig::default(),
      true,
    )
    .unwrap()
    .unwrap();

    assert_eq!(headers["content-type"], "application/octet-stream");
    assert!(body.is_empty());
  }

  #[test]
  fn text_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/plain;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_csp(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      "default-src 'self'",
      format!(".*<html lang=en data-inscription={inscription_id}>.*"),
    );
  }

  #[test]
  fn audio_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("audio/flac", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<audio .*>\s*<source src=/content/{inscription_id}>.*"),
    );
  }

  #[test]
  fn font_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("font/ttf", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*src: url\(/content/{inscription_id}\).*"),
    );
  }

  #[test]
  fn pdf_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("application/pdf", "hello").to_witness(),
      )],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<canvas data-inscription={inscription_id}></canvas>.*"),
    );
  }

  #[test]
  fn markdown_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/markdown", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<html lang=en data-inscription={inscription_id}>.*"),
    );
  }

  #[test]
  fn image_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("image/png", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_csp(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      "default-src 'self' 'unsafe-inline'",
      format!(r".*background-image: url\(/content/{inscription_id}\);.*"),
    );
  }

  #[test]
  fn iframe_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/html;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_csp(
      format!("/preview/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      "default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:",
      "hello",
    );
  }

  #[test]
  fn unknown_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_csp(
      format!("/preview/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      "default-src 'self'",
      fs::read_to_string("templates/preview-unknown.html").unwrap(),
    );
  }

  #[test]
  fn video_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("video/webm", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<video .*>\s*<source src=/content/{inscription_id}>.*"),
    );
  }

  #[test]
  fn inscription_page_title() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      ".*<title>Inscription 0</title>.*",
    );
  }

  #[test]
  fn inscription_page_has_sat_when_sats_are_tracked() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      r".*<dt>sat</dt>\s*<dd><a href=/sat/5000000000>5000000000</a></dd>\s*<dt>sat name</dt>\s*<dd><a href=/sat/nvtcsezkbth>nvtcsezkbth</a></dd>\s*<dt>preview</dt>.*",
    );
  }

  #[test]
  fn inscriptions_can_be_looked_up_by_sat_name() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{}", Sat(5000000000).name()),
      StatusCode::OK,
      ".*<title>Inscription 0</title.*",
    );
  }

  #[test]
  fn gallery_items_can_be_looked_up_by_gallery_sat_name() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        Inscription {
          content_type: Some("test/foo".into()),
          body: Some("foo".into()),
          properties: Properties {
            gallery: vec![Item {
              id: Some(inscription_id(1)),
              ..default()
            }],
            ..default()
          }
          .to_inline_cbor(),
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/gallery/{}/0", Sat(5000000000).name()),
      StatusCode::OK,
      ".*<title>Gallery 0 Item 0</title.*",
    );
  }

  #[test]
  fn inscriptions_can_be_looked_up_by_sat_name_with_letter_i() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.assert_response_regex("/inscription/i", StatusCode::NOT_FOUND, ".*");
  }

  #[test]
  fn inscription_page_does_not_have_sat_when_sats_are_not_tracked() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      r".*<dt>value</dt>\s*<dd>5000000000</dd>\s*<dt>preview</dt>.*",
    );
  }

  #[test]
  fn strict_transport_security_header_is_set() {
    assert_eq!(
      TestServer::new()
        .get("/status")
        .headers()
        .get(header::STRICT_TRANSPORT_SECURITY)
        .unwrap(),
      "max-age=31536000; includeSubDomains; preload",
    );
  }

  #[test]
  fn feed() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/feed.xml",
      StatusCode::OK,
      ".*<title>Inscription 0</title>.*",
    );
  }

  #[test]
  fn inscription_with_unknown_type_and_no_body_has_unknown_preview() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        Inscription {
          content_type: Some("foo/bar".as_bytes().to_vec()),
          body: None,
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      &fs::read_to_string("templates/preview-unknown.html").unwrap(),
    );
  }

  #[test]
  fn inscription_with_known_type_and_no_body_has_unknown_preview() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        Inscription {
          content_type: Some("image/png".as_bytes().to_vec()),
          body: None,
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      &fs::read_to_string("templates/preview-unknown.html").unwrap(),
    );
  }

  #[test]
  fn content_responses_have_cache_control_headers() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let response = server.get(format!("/content/{}", InscriptionId { txid, index: 0 }));

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
      response.headers().get(header::CACHE_CONTROL).unwrap(),
      "public, max-age=1209600, immutable"
    );
  }

  #[test]
  fn error_content_responses_have_max_age_zero_cache_control_headers() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    let response =
      server.get("/content/6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0");

    assert_eq!(response.status(), 404);
    assert_eq!(
      response.headers().get(header::CACHE_CONTROL).unwrap(),
      "no-store"
    );
  }

  #[test]
  fn inscriptions_page_with_no_prev_or_next() {
    TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build()
      .assert_response_regex("/inscriptions", StatusCode::OK, ".*prev\nnext.*");
  }

  #[test]
  fn inscriptions_page_with_no_next() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    for i in 0..101 {
      server.mine_blocks(1);
      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/inscriptions/1",
      StatusCode::OK,
      ".*<a class=prev href=/inscriptions/0>prev</a>\nnext.*",
    );
  }

  #[test]
  fn inscriptions_page_with_no_prev() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    for i in 0..101 {
      server.mine_blocks(1);
      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/inscriptions/0",
      StatusCode::OK,
      ".*prev\n<a class=next href=/inscriptions/1>next</a>.*",
    );
  }

  #[test]
  fn collections_page_prev_and_next() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    let mut parent_ids = Vec::new();

    for i in 0..101 {
      server.mine_blocks(1);

      parent_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(i + 1, 0, 0, inscription("image/png", "hello").to_witness())],
          ..default()
        }),
        index: 0,
      });
    }

    for (i, parent_id) in parent_ids.iter().enumerate().take(101) {
      server.mine_blocks(1);

      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (i + 2, 1, 0, Default::default()),
          (
            i + 102,
            0,
            0,
            Inscription {
              content_type: Some("text/plain".into()),
              body: Some("hello".into()),
              parents: vec![parent_id.value()],
              ..default()
            }
            .to_witness(),
          ),
        ],
        outputs: 2,
        output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/collections",
      StatusCode::OK,
      r".*
<h1>Collections</h1>
<div class=thumbnails>
  <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
  (<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>\s*){99}
</div>
<div class=center>
prev
<a class=next href=/collections/1>next</a>
</div>.*"
        .to_string()
        .unindent(),
    );

    server.assert_response_regex(
      "/collections/1",
      StatusCode::OK,
      ".*
<h1>Collections</h1>
<div class=thumbnails>
  <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
</div>
<div class=center>
<a class=prev href=/collections/0>prev</a>
next
</div>.*"
        .unindent(),
    );
  }

  #[test]
  fn collections_page_ordered_by_most_recent_child() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);
    let parent_a = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("image/png", "parent a").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);
    let parent_b = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("image/png", "parent b").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);
    let parent_c = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 0, 0, inscription("image/png", "parent c").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);
    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (2, 1, 0, Default::default()),
        (
          4,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("child a1".into()),
            parents: vec![parent_a.value()],
            ..default()
          }
          .to_witness(),
        ),
      ],
      outputs: 2,
      output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
      ..default()
    });

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (3, 1, 0, Default::default()),
        (
          5,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("child b1".into()),
            parents: vec![parent_b.value()],
            ..default()
          }
          .to_witness(),
        ),
      ],
      outputs: 2,
      output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
      ..default()
    });

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (4, 1, 0, Default::default()),
        (
          6,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("child c1".into()),
            parents: vec![parent_c.value()],
            ..default()
          }
          .to_witness(),
        ),
      ],
      outputs: 2,
      output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/collections",
      StatusCode::OK,
      format!(
        ".*<h1>Collections</h1>\n<div class=thumbnails>\n  <a href=/inscription/{parent_c}>.*</a>\n  <a href=/inscription/{parent_b}>.*</a>\n  <a href=/inscription/{parent_a}>.*</a>\n</div>.*"
      ),
    );

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (6, 1, 0, Default::default()),
        (
          7,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("child a2".into()),
            parents: vec![parent_a.value()],
            ..default()
          }
          .to_witness(),
        ),
      ],
      outputs: 2,
      output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/collections",
      StatusCode::OK,
      format!(
        ".*<h1>Collections</h1>\n<div class=thumbnails>\n  <a href=/inscription/{parent_a}>.*</a>\n  <a href=/inscription/{parent_c}>.*</a>\n  <a href=/inscription/{parent_b}>.*</a>\n</div>.*"
      ),
    );
  }

  #[test]
  fn collections_page_shows_both_parents_of_multi_parent_child() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);
    let parent_a = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("image/png", "parent a").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);
    let parent_b = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("image/png", "parent b").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (2, 1, 0, Default::default()),
        (3, 1, 0, Default::default()),
        (
          3,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("child".into()),
            parents: vec![parent_a.value(), parent_b.value()],
            ..default()
          }
          .to_witness(),
        ),
      ],
      outputs: 3,
      output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE, 50 * COIN_VALUE],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/collections",
      StatusCode::OK,
      format!(
        ".*<h1>Collections</h1>\n<div class=thumbnails>\n  <a href=/inscription/{parent_a}>.*</a>\n  <a href=/inscription/{parent_b}>.*</a>\n</div>.*"
      ),
    );
  }

  #[test]
  fn galleries_page_prev_and_next() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    let mut gallery_item_ids = Vec::new();

    for i in 0..15 {
      server.mine_blocks(1);

      gallery_item_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(
            i + 1,
            0,
            0,
            inscription("image/png", "gallery item").to_witness(),
          )],
          ..default()
        }),
        index: 0,
      });
    }

    for i in 0..101 {
      server.mine_blocks(1);

      let gallery_items = gallery_item_ids
        .iter()
        .cycle()
        .skip((i * 3) % gallery_item_ids.len())
        .take(3)
        .map(|&id| properties::Item {
          id: Some(id),
          attributes: Attributes::default(),
          index: None,
        })
        .collect::<Vec<_>>();

      let properties = Properties {
        attributes: Attributes::default(),
        gallery: gallery_items,
        txids: Vec::new(),
      };

      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          i + 16,
          0,
          0,
          Inscription {
            content_type: Some("image/png".into()),
            body: Some("gallery".into()),
            properties: properties.to_inline_cbor(),
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/galleries",
      StatusCode::OK,
      r".*
<h1>Galleries</h1>
<div class=thumbnails>
  <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
  (<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>\s*){99}
</div>
<div class=center>
prev
<a class=next href=/galleries/1>next</a>
</div>.*"
        .to_string()
        .unindent(),
    );

    server.assert_response_regex(
      "/galleries/1",
      StatusCode::OK,
      ".*
<h1>Galleries</h1>
<div class=thumbnails>
  <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
</div>
<div class=center>
<a class=prev href=/galleries/0>prev</a>
next
</div>.*"
        .unindent(),
    );

    server.assert_response_regex(
      "/galleries",
      StatusCode::OK,
      r".*<a href=/galleries title=galleries><img class=icon src=/static/box-archive\.svg></a>.*",
    );
  }

  #[test]
  fn gallery_page_prev_and_next() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    let mut gallery_item_ids = Vec::new();

    for i in 0..101 {
      server.mine_blocks(1);

      gallery_item_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(
            i + 1,
            0,
            0,
            inscription("text/plain", "gallery item").to_witness(),
          )],
          ..default()
        }),
        index: 0,
      });
    }

    let gallery_items = gallery_item_ids
      .iter()
      .map(|&id| properties::Item {
        id: Some(id),
        attributes: Attributes::default(),
        index: None,
      })
      .collect::<Vec<_>>();

    let properties = Properties {
      attributes: Attributes::default(),
      gallery: gallery_items,
      txids: Vec::new(),
    };

    server.mine_blocks(1);

    let gallery_id = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          102,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("gallery".into()),
            properties: properties.to_inline_cbor(),
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);

    let response = server.get(format!("/gallery/{gallery_id}"));
    assert_eq!(response.status(), StatusCode::OK);
    let body = response.text().unwrap();
    assert!(body.contains(&format!(
      "<h1><a href=/inscription/{gallery_id}>Inscription"
    )));
    assert!(body.contains(&format!("href=/gallery/{gallery_id}/0")));
    assert!(body.contains(&format!(
      "<a class=next href=/gallery/{gallery_id}/page/1>next</a>"
    )));

    let response = server.get(format!("/gallery/{gallery_id}/page/1"));
    assert_eq!(response.status(), StatusCode::OK);
    let body = response.text().unwrap();
    assert!(body.contains(&format!(
      "<h1><a href=/inscription/{gallery_id}>Inscription"
    )));
    assert!(body.contains(&format!("href=/gallery/{gallery_id}/100")));
    assert!(body.contains(&format!(
      "<a class=prev href=/gallery/{gallery_id}/page/0>prev</a>"
    )));
  }

  #[test]
  fn galleries_json() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    let item_id = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("image/png", "foo").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);

    let gallery_id = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          0,
          0,
          Inscription {
            content_type: Some("image/png".into()),
            body: Some("bar".into()),
            properties: Properties {
              gallery: vec![Item {
                id: Some(item_id),
                ..default()
              }],
              ..default()
            }
            .to_inline_cbor(),
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);

    let json: api::Inscriptions = server.get_json("/galleries");

    assert_eq!(json.ids, vec![gallery_id]);
    assert!(!json.more);
    assert_eq!(json.page_index, 0);
  }

  #[test]
  fn galleries_json_pagination() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    let mut item_ids = Vec::new();

    for i in 0..3 {
      server.mine_blocks(1);

      item_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(i + 1, 0, 0, inscription("image/png", "foo").to_witness())],
          ..default()
        }),
        index: 0,
      });
    }

    let mut gallery_ids = Vec::new();

    for i in 0..101 {
      server.mine_blocks(1);

      gallery_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(
            i + 4,
            0,
            0,
            Inscription {
              content_type: Some("image/png".into()),
              body: Some("bar".into()),
              properties: Properties {
                gallery: vec![Item {
                  id: Some(item_ids[i % item_ids.len()]),
                  ..default()
                }],
                ..default()
              }
              .to_inline_cbor(),
              ..default()
            }
            .to_witness(),
          )],
          ..default()
        }),
        index: 0,
      });
    }

    server.mine_blocks(1);

    let json: api::Inscriptions = server.get_json("/galleries");

    assert_eq!(json.ids.len(), 100);
    assert!(json.more);
    assert_eq!(json.page_index, 0);

    let json: api::Inscriptions = server.get_json("/galleries/1");

    assert_eq!(json.ids.len(), 1);
    assert!(!json.more);
    assert_eq!(json.page_index, 1);
  }

  #[test]
  fn non_gallery_inscription_not_in_galleries() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let json: api::Inscriptions = server.get_json("/galleries");

    assert!(json.ids.is_empty());
    assert!(!json.more);
  }

  #[test]
  fn hidden_galleries_are_excluded() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    let item_id = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("image/png", "foo").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        2,
        0,
        0,
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("bar".into()),
          properties: Properties {
            gallery: vec![Item {
              id: Some(item_id),
              ..default()
            }],
            ..default()
          }
          .to_inline_cbor(),
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    let visible_gallery_id = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          3,
          0,
          0,
          Inscription {
            content_type: Some("image/png".into()),
            body: Some("baz".into()),
            properties: Properties {
              gallery: vec![Item {
                id: Some(item_id),
                ..default()
              }],
              ..default()
            }
            .to_inline_cbor(),
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);

    let json: api::Inscriptions = server.get_json("/galleries");

    assert_eq!(json.ids, vec![visible_gallery_id]);
  }

  #[test]
  fn hidden_collections_are_excluded() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    let hidden_parent = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);

    let visible_parent = InscriptionId {
      txid: server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("image/png", "bar").to_witness())],
        ..default()
      }),
      index: 0,
    };

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (2, 1, 0, Default::default()),
        (
          3,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("baz".into()),
            parents: vec![hidden_parent.value()],
            ..default()
          }
          .to_witness(),
        ),
      ],
      outputs: 2,
      output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
      ..default()
    });

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (3, 1, 0, Default::default()),
        (
          4,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("qux".into()),
            parents: vec![visible_parent.value()],
            ..default()
          }
          .to_witness(),
        ),
      ],
      outputs: 2,
      output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/collections",
      StatusCode::OK,
      format!(
        ".*<h1>Collections</h1>\n<div class=thumbnails>\n  <a href=/inscription/{visible_parent}>.*</a>\n</div>.*"
      ),
    );
  }

  #[test]
  fn responses_are_gzipped() {
    let server = TestServer::new();

    let mut headers = HeaderMap::new();

    headers.insert(header::ACCEPT_ENCODING, "gzip".parse().unwrap());

    let response = reqwest::blocking::Client::builder()
      .default_headers(headers)
      .build()
      .unwrap()
      .get(server.join_url("/"))
      .send()
      .unwrap();

    assert_eq!(
      response.headers().get(header::CONTENT_ENCODING).unwrap(),
      "gzip"
    );
  }

  #[test]
  fn responses_are_brotlied() {
    let server = TestServer::new();

    let mut headers = HeaderMap::new();

    headers.insert(header::ACCEPT_ENCODING, BROTLI.parse().unwrap());

    let response = reqwest::blocking::Client::builder()
      .default_headers(headers)
      .brotli(false)
      .build()
      .unwrap()
      .get(server.join_url("/"))
      .send()
      .unwrap();

    assert_eq!(
      response.headers().get(header::CONTENT_ENCODING).unwrap(),
      BROTLI
    );
  }

  #[test]
  fn inscription_links_to_parent() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (2, 1, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription 1</title>.*<dt>parents</dt>.*<div class=thumbnails>.**<a href=/inscription/{parent_inscription_id}><iframe .* src=/preview/{parent_inscription_id}></iframe></a>.*"),
    );
    server.assert_response_regex(
      format!("/inscription/{parent_inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription 0</title>.*<dt>children</dt>.*<a href=/inscription/{inscription_id}>.*</a>.*"),
    );

    assert_eq!(
      server
        .get_json::<api::Inscription>(format!("/inscription/{inscription_id}"))
        .parents,
      vec![parent_inscription_id],
    );

    assert_eq!(
      server
        .get_json::<api::Inscription>(format!("/inscription/{parent_inscription_id}"))
        .children,
      [inscription_id],
    );
  }

  #[test]
  fn inscription_with_and_without_children_page() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response_regex(
      format!("/children/{parent_inscription_id}"),
      StatusCode::OK,
      ".*<h3>No children</h3>.*",
    );

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (2, 1, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/children/{parent_inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription 0 Children</title>.*<h1><a href=/inscription/{parent_inscription_id}>Inscription 0</a> Children</h1>.*<div class=thumbnails>.*<a href=/inscription/{inscription_id}><iframe .* src=/preview/{inscription_id}></iframe></a>.*"),
    );
  }

  #[test]
  fn inscription_with_and_without_gallery_page() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let empty_gallery_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let empty_gallery_id = InscriptionId {
      txid: empty_gallery_txid,
      index: 0,
    };

    server.assert_response_regex(
      format!("/gallery/{empty_gallery_id}"),
      StatusCode::OK,
      ".*<h3>No gallery items</h3>.*",
    );

    let item_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, inscription("text/plain", "item").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let item_id = InscriptionId {
      txid: item_txid,
      index: 0,
    };

    let gallery_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        3,
        0,
        0,
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("gallery".into()),
          properties: Properties {
            gallery: vec![Item {
              id: Some(item_id),
              ..default()
            }],
            ..default()
          }
          .to_inline_cbor(),
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    let gallery_id = InscriptionId {
      txid: gallery_txid,
      index: 0,
    };

    server.assert_response_regex(
      format!("/gallery/{gallery_id}"),
      StatusCode::OK,
      format!(
        ".*<title>Inscription \\d+ Gallery</title>.*<h1><a href=/inscription/{gallery_id}>Inscription \\d+</a> Gallery</h1>.*<div class=thumbnails>.*<a href=/gallery/{gallery_id}/0><iframe .* src=/preview/{item_id}></iframe></a>.*",
      ),
    );
  }

  #[test]
  fn inscriptions_page_shows_max_four_children() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(6);

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    let _txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          3,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          4,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          5,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          6,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (2, 1, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{parent_inscription_id}"),
      StatusCode::OK,
      format!(
        ".*<title>Inscription 0</title>.*
.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
    <div class=center>
      <a href=/children/{parent_inscription_id}>all \\(5\\)</a>
    </div>.*"
      ),
    );
  }

  #[test]
  fn inscriptions_page_shows_max_four_gallery_items() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    let mut item_ids = Vec::new();

    for i in 0..5 {
      server.mine_blocks(1);

      item_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(
            i + 1,
            0,
            0,
            inscription("text/plain", "gallery item").to_witness(),
          )],
          ..default()
        }),
        index: 0,
      });
    }

    let gallery_items = item_ids
      .into_iter()
      .map(|id| Item {
        id: Some(id),
        ..default()
      })
      .collect::<Vec<_>>();

    let properties = Properties {
      attributes: Attributes::default(),
      gallery: gallery_items,
      txids: Vec::new(),
    };

    server.mine_blocks(1);

    let gallery_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        6,
        0,
        0,
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("gallery".into()),
          properties: properties.to_inline_cbor(),
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    let gallery_id = InscriptionId {
      txid: gallery_txid,
      index: 0,
    };

    server.assert_response_regex(
      format!("/inscription/{gallery_id}"),
      StatusCode::OK,
      format!(
        ".*<title>Inscription \\d+</title>.*
.*<dt>gallery</dt>.*
.*<a href=/gallery/{gallery_id}/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/gallery/{gallery_id}/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/gallery/{gallery_id}/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/gallery/{gallery_id}/.*><iframe .* src=/preview/.*></iframe></a>.*
    <div class=center>
      <a href=/gallery/{gallery_id}>all \\(5\\)</a>
    </div>.*"
      ),
    );
  }

  #[test]
  fn inscription_child() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(2);

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    let child_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          3,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (2, 1, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let child0 = InscriptionId {
      txid: child_txid,
      index: 0,
    };

    server.assert_response_regex(
      format!("/inscription/{parent_inscription_id}/0"),
      StatusCode::OK,
      format!(
        ".*<title>Inscription 1</title>.*
.*<dt>id</dt>
.*<dd class=collapse>{child0}</dd>.*"
      ),
    );

    let child1 = InscriptionId {
      txid: child_txid,
      index: 1,
    };

    server.assert_response_regex(
      format!("/inscription/{parent_inscription_id}/1"),
      StatusCode::OK,
      format!(
        ".*<title>Inscription -1</title>.*
.*<dt>id</dt>
.*<dd class=collapse>{child1}</dd>.*"
      ),
    );
  }

  #[test]
  fn inscription_with_parent_page() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(2);

    let parent_a_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_b_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let parent_a_inscription_id = InscriptionId {
      txid: parent_a_txid,
      index: 0,
    };

    let parent_b_inscription_id = InscriptionId {
      txid: parent_b_txid,
      index: 0,
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          3,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![
              parent_a_inscription_id.value(),
              parent_b_inscription_id.value(),
            ],
            ..default()
          }
          .to_witness(),
        ),
        (3, 1, 0, Default::default()),
        (3, 2, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/parents/{inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>.*<a href=/inscription/{parent_a_inscription_id}><iframe .* src=/preview/{parent_b_inscription_id}></iframe></a>.*"),
    );
  }

  #[test]
  fn inscription_parent_page_pagination() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let mut parent_ids = Vec::new();
    let mut inputs = Vec::new();
    for i in 0..101 {
      parent_ids.push(
        InscriptionId {
          txid: server.core.broadcast_tx(TransactionTemplate {
            inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
            ..default()
          }),
          index: 0,
        }
        .value(),
      );

      inputs.push((i + 2, 1, 0, Witness::default()));

      server.mine_blocks(1);
    }

    inputs.insert(
      0,
      (
        102,
        0,
        0,
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("hello".into()),
          parents: parent_ids,
          ..default()
        }
        .to_witness(),
      ),
    );

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &inputs,
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/parents/{inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){{100}}.*"),
    );

    server.assert_response_regex(
      format!("/parents/{inscription_id}/1"),
      StatusCode::OK,
      format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){{1}}.*"),
    );

    server.assert_response_regex(
      format!("/inscription/{inscription_id}"),
      StatusCode::OK,
      ".*<title>Inscription -1</title>.*<h1>Inscription -1</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){4}.*",
    );
  }

  #[test]
  fn inscription_number_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(2);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (1, 0, 0, inscription("text/plain", "hello").to_witness()),
        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
      ],
      outputs: 2,
      ..default()
    });

    let inscription_id = InscriptionId { txid, index: 0 };
    let cursed_inscription_id = InscriptionId { txid, index: 1 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{inscription_id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{inscription_id}</dd>.*"
      ),
    );
    server.assert_response_regex(
      "/inscription/0",
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{inscription_id}</dd>.*"
      ),
    );

    server.assert_response_regex(
      "/inscription/-1",
      StatusCode::OK,
      format!(
        ".*<h1>Inscription -1</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{cursed_inscription_id}</dd>.*"
      ),
    )
  }

  #[test]
  fn charm_cursed() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(2);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (1, 0, 0, Witness::default()),
        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
      ],
      outputs: 2,
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription -1</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>
    <span title=cursed>👹</span>
  </dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_vindicated() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(110);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (1, 0, 0, Witness::default()),
        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
      ],
      outputs: 2,
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  .*
  <dt>value</dt>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_coin() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(2);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>.*<span title=coin>🪙</span>.*</dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_uncommon() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(2);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>.*<span title=uncommon>🌱</span>.*</dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_nineball() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(9);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(9, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>.*<span title=nineball>9️⃣</span>.*</dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_reinscription() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 1, 0, inscription("text/plain", "bar").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription -1</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>
    <span title=reinscription>♻️</span>
    <span title=cursed>👹</span>
  </dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_reinscription_in_same_tx_input() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(b"ord")
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"foo")
      .push_opcode(opcodes::all::OP_ENDIF)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(b"ord")
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"bar")
      .push_opcode(opcodes::all::OP_ENDIF)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(b"ord")
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"qix")
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, witness)],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  .*
  <dt>value</dt>
  .*
</dl>
.*
"
      ),
    );

    let id = InscriptionId { txid, index: 1 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      ".*
    <span title=reinscription>♻️</span>
    <span title=cursed>👹</span>.*",
    );

    let id = InscriptionId { txid, index: 2 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      ".*
    <span title=reinscription>♻️</span>
    <span title=cursed>👹</span>.*",
    );
  }

  #[test]
  fn charm_reinscription_in_same_tx_with_pointer() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(3);

    let cursed_inscription = inscription("text/plain", "bar");
    let reinscription: Inscription = InscriptionTemplate {
      pointer: Some(0),
      ..default()
    }
    .into();

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (1, 0, 0, inscription("text/plain", "foo").to_witness()),
        (2, 0, 0, cursed_inscription.to_witness()),
        (3, 0, 0, reinscription.to_witness()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  .*
  <dt>value</dt>
  .*
</dl>
.*
"
      ),
    );

    let id = InscriptionId { txid, index: 1 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      ".*
    <span title=cursed>👹</span>.*",
    );

    let id = InscriptionId { txid, index: 2 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      ".*
    <span title=reinscription>♻️</span>
    <span title=cursed>👹</span>.*",
    );
  }

  #[test]
  fn charm_unbound() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, envelope(&[b"ord", &[128], &[0]]))],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription -1</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>
    <span title=cursed>👹</span>
    <span title=unbound>🔓</span>
  </dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_lost() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  .*
  <dt>value</dt>
  <dd>5000000000</dd>
  .*
</dl>
.*
"
      ),
    );

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 1, 0, Default::default())],
      fee: 50 * COIN_VALUE,
      ..default()
    });

    server.mine_blocks_with_subsidy(1, 0);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>
    <span title=lost>🤔</span>
  </dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn utxo_recursive_endpoint_all() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .index_runes()
      .build();

    let rune = Rune(RUNE);

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          divisibility: Some(1),
          rune: Some(rune),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          divisibility: 1,
          etching: txid,
          spaced_rune: SpacedRune { rune, spacers: 0 },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        }
      )]
    );

    server.mine_blocks(1);

    // merge rune with two inscriptions
    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (6, 0, 0, inscription("text/plain", "foo").to_witness()),
        (7, 0, 0, inscription("text/plain", "bar").to_witness()),
        (7, 1, 0, Witness::new()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };
    let second_inscription_id = InscriptionId { txid, index: 1 };
    let outpoint: OutPoint = OutPoint { txid, vout: 0 };

    let utxo_recursive = server.get_json::<api::UtxoRecursive>(format!("/r/utxo/{outpoint}"));

    pretty_assert_eq!(
      utxo_recursive,
      api::UtxoRecursive {
        inscriptions: Some(vec![inscription_id, second_inscription_id]),
        runes: Some(
          [(
            SpacedRune { rune, spacers: 0 },
            Pile {
              amount: u128::MAX,
              divisibility: 1,
              symbol: None
            }
          )]
          .into_iter()
          .collect()
        ),
        sat_ranges: Some(vec![
          (6 * 50 * COIN_VALUE, 7 * 50 * COIN_VALUE),
          (7 * 50 * COIN_VALUE, 8 * 50 * COIN_VALUE),
          (50 * COIN_VALUE, 2 * 50 * COIN_VALUE)
        ]),
        value: 150 * COIN_VALUE,
      }
    );
  }

  #[test]
  fn utxo_recursive_endpoint_only_inscriptions() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };
    let outpoint: OutPoint = OutPoint { txid, vout: 0 };

    let utxo_recursive = server.get_json::<api::UtxoRecursive>(format!("/r/utxo/{outpoint}"));

    pretty_assert_eq!(
      utxo_recursive,
      api::UtxoRecursive {
        inscriptions: Some(vec![inscription_id]),
        runes: None,
        sat_ranges: None,
        value: 50 * COIN_VALUE,
      }
    );
  }

  #[test]
  fn sat_recursive_endpoints() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    assert_eq!(
      server.get_json::<api::SatInscriptions>("/r/sat/5000000000"),
      api::SatInscriptions {
        ids: Vec::new(),
        page: 0,
        more: false
      }
    );

    assert_eq!(
      server.get_json::<api::SatInscription>("/r/sat/5000000000/at/0"),
      api::SatInscription { id: None }
    );

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let mut ids = Vec::new();
    ids.push(InscriptionId { txid, index: 0 });

    for i in 1..111 {
      let txid = server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 1, 0, inscription("text/plain", "foo").to_witness())],
        ..default()
      });

      server.mine_blocks(1);

      ids.push(InscriptionId { txid, index: 0 });
    }

    let paginated_response = server.get_json::<api::SatInscriptions>("/r/sat/5000000000");

    let equivalent_paginated_response =
      server.get_json::<api::SatInscriptions>("/r/sat/5000000000/0");

    assert_eq!(paginated_response.ids.len(), 100);
    assert!(paginated_response.more);
    assert_eq!(paginated_response.page, 0);

    assert_eq!(
      paginated_response.ids.len(),
      equivalent_paginated_response.ids.len()
    );
    assert_eq!(paginated_response.more, equivalent_paginated_response.more);
    assert_eq!(paginated_response.page, equivalent_paginated_response.page);

    let paginated_response = server.get_json::<api::SatInscriptions>("/r/sat/5000000000/1");

    assert_eq!(paginated_response.ids.len(), 11);
    assert!(!paginated_response.more);
    assert_eq!(paginated_response.page, 1);

    assert_eq!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/0")
        .id,
      Some(ids[0])
    );

    assert_eq!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/-111")
        .id,
      Some(ids[0])
    );

    assert_eq!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/110")
        .id,
      Some(ids[110])
    );

    assert_eq!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/-1")
        .id,
      Some(ids[110])
    );

    assert!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/111")
        .id
        .is_none()
    );

    assert_eq!(
      server
        .get("/r/sat/5000000000/at/0")
        .headers()
        .get(header::CACHE_CONTROL),
      None,
    );

    assert_eq!(
      server
        .get("/r/sat/5000000000/at/0/content")
        .headers()
        .get(header::CACHE_CONTROL)
        .unwrap(),
      "public, max-age=1209600, immutable",
    );

    assert_eq!(
      server
        .get("/r/sat/5000000000/at/-1")
        .headers()
        .get(header::CACHE_CONTROL)
        .unwrap(),
      "no-store",
    );

    assert_eq!(
      server
        .get("/r/sat/5000000000/at/-1/content")
        .headers()
        .get(header::CACHE_CONTROL)
        .unwrap(),
      "no-store",
    );
  }

  #[test]
  fn children_recursive_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/children/{parent_inscription_id}"),
      StatusCode::NOT_FOUND,
      &format!("inscription {parent_inscription_id} not found"),
    );

    server.mine_blocks(1);

    let children_json =
      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}"));
    assert_eq!(children_json.ids.len(), 0);

    let mut builder = script::Builder::new();
    for _ in 0..111 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![parent_inscription_id.value()],
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    server.mine_blocks(1);

    let first_child_inscription_id = InscriptionId { txid, index: 0 };
    let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
    let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
    let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };

    let children_json =
      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}"));

    assert_eq!(children_json.ids.len(), 100);
    assert_eq!(children_json.ids[0], first_child_inscription_id);
    assert_eq!(children_json.ids[99], hundredth_child_inscription_id);
    assert!(children_json.more);
    assert_eq!(children_json.page, 0);

    let children_json =
      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}/1"));

    assert_eq!(children_json.ids.len(), 11);
    assert_eq!(children_json.ids[0], hundred_first_child_inscription_id);
    assert_eq!(children_json.ids[10], hundred_eleventh_child_inscription_id);
    assert!(!children_json.more);
    assert_eq!(children_json.page, 1);
  }

  #[test]
  fn children_json_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response(
      format!("/children/{parent_inscription_id}"),
      StatusCode::NOT_FOUND,
      &format!("inscription {parent_inscription_id} not found"),
    );

    server.mine_blocks(1);

    let children_json =
      server.get_json::<api::Children>(format!("/children/{parent_inscription_id}"));
    assert_eq!(children_json.ids.len(), 0);
    assert!(!children_json.more);
    assert_eq!(children_json.page, 0);

    let mut builder = script::Builder::new();
    for _ in 0..111 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![parent_inscription_id.value()],
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    server.mine_blocks(1);

    let first_child_inscription_id = InscriptionId { txid, index: 0 };
    let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
    let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
    let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };

    let children_json =
      server.get_json::<api::Children>(format!("/children/{parent_inscription_id}"));

    assert_eq!(children_json.ids.len(), 100);
    assert_eq!(children_json.ids[0], first_child_inscription_id);
    assert_eq!(children_json.ids[99], hundredth_child_inscription_id);
    assert!(children_json.more);
    assert_eq!(children_json.page, 0);

    let children_json =
      server.get_json::<api::Children>(format!("/children/{parent_inscription_id}/1"));

    assert_eq!(children_json.ids.len(), 11);
    assert_eq!(children_json.ids[0], hundred_first_child_inscription_id);
    assert_eq!(children_json.ids[10], hundred_eleventh_child_inscription_id);
    assert!(!children_json.more);
    assert_eq!(children_json.page, 1);
  }

  #[test]
  fn parents_recursive_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let mut parent_ids = Vec::new();
    let mut inputs = Vec::new();
    for i in 0..111 {
      parent_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
          ..default()
        }),
        index: 0,
      });

      inputs.push((i + 2, 1, 0, Witness::default()));

      server.mine_blocks(1);
    }

    inputs.insert(
      0,
      (
        112,
        0,
        0,
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("hello".into()),
          parents: parent_ids.iter().map(|id| id.value()).collect(),
          ..default()
        }
        .to_witness(),
      ),
    );

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &inputs,
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    let first_parent_inscription_id = parent_ids[0];
    let hundredth_parent_inscription_id = parent_ids[99];
    let hundred_first_parent_inscription_id = parent_ids[100];
    let hundred_eleventh_parent_inscription_id = parent_ids[110];

    let parents_json = server.get_json::<api::Inscriptions>(format!("/r/parents/{inscription_id}"));

    assert_eq!(parents_json.ids.len(), 100);
    assert_eq!(parents_json.ids[0], first_parent_inscription_id);
    assert_eq!(parents_json.ids[99], hundredth_parent_inscription_id);
    assert!(parents_json.more);
    assert_eq!(parents_json.page_index, 0);

    let parents_json =
      server.get_json::<api::Inscriptions>(format!("/r/parents/{inscription_id}/1"));

    assert_eq!(parents_json.ids.len(), 11);
    assert_eq!(parents_json.ids[0], hundred_first_parent_inscription_id);
    assert_eq!(parents_json.ids[10], hundred_eleventh_parent_inscription_id);
    assert!(!parents_json.more);
    assert_eq!(parents_json.page_index, 1);
  }

  #[test]
  fn child_inscriptions_recursive_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/children/{parent_inscription_id}/inscriptions"),
      StatusCode::NOT_FOUND,
      &format!("inscription {parent_inscription_id} not found"),
    );

    server.mine_blocks(1);

    let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
      "/r/children/{parent_inscription_id}/inscriptions"
    ));
    assert_eq!(child_inscriptions_json.children.len(), 0);

    let mut builder = script::Builder::new();
    for _ in 0..111 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![parent_inscription_id.value()],
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    server.mine_blocks(1);

    let first_child_inscription_id = InscriptionId { txid, index: 0 };
    let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
    let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
    let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };

    let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
      "/r/children/{parent_inscription_id}/inscriptions"
    ));

    assert_eq!(child_inscriptions_json.children.len(), 100);

    assert_eq!(
      child_inscriptions_json.children[0].id,
      first_child_inscription_id
    );
    assert_eq!(child_inscriptions_json.children[0].number, 1); // parent is #0, 1st child is #1

    assert_eq!(
      child_inscriptions_json.children[99].id,
      hundredth_child_inscription_id
    );
    assert_eq!(child_inscriptions_json.children[99].number, -99); // all but 1st child are cursed

    assert!(child_inscriptions_json.more);
    assert_eq!(child_inscriptions_json.page, 0);

    let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
      "/r/children/{parent_inscription_id}/inscriptions/1"
    ));

    assert_eq!(child_inscriptions_json.children.len(), 11);

    assert_eq!(
      child_inscriptions_json.children[0].id,
      hundred_first_child_inscription_id
    );
    assert_eq!(child_inscriptions_json.children[0].number, -100);

    assert_eq!(
      child_inscriptions_json.children[10].id,
      hundred_eleventh_child_inscription_id
    );
    assert_eq!(child_inscriptions_json.children[10].number, -110);

    assert!(!child_inscriptions_json.more);
    assert_eq!(child_inscriptions_json.page, 1);
  }

  #[test]
  fn parent_inscriptions_recursive_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let mut builder = script::Builder::new();
    for _ in 0..111 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let parents_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, witness)],
      ..default()
    });

    server.mine_blocks(1);

    let mut builder = script::Builder::new();
    builder = Inscription {
      content_type: Some("text/plain".into()),
      body: Some("hello".into()),
      parents: (0..111)
        .map(|i| {
          InscriptionId {
            txid: parents_txid,
            index: i,
          }
          .value()
        })
        .collect(),
      unrecognized_even_field: false,
      ..default()
    }
    .append_reveal_script_to_builder(builder);

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let child_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    let child_inscription_id = InscriptionId {
      txid: child_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/parents/{child_inscription_id}/inscriptions"),
      StatusCode::NOT_FOUND,
      &format!("inscription {child_inscription_id} not found"),
    );

    server.mine_blocks(1);

    let first_parent_inscription_id = InscriptionId {
      txid: parents_txid,
      index: 0,
    };
    let hundredth_parent_inscription_id = InscriptionId {
      txid: parents_txid,
      index: 99,
    };
    let hundred_first_parent_inscription_id = InscriptionId {
      txid: parents_txid,
      index: 100,
    };
    let hundred_eleventh_parent_inscription_id = InscriptionId {
      txid: parents_txid,
      index: 110,
    };

    let parent_inscriptions_json = server.get_json::<api::ParentInscriptions>(format!(
      "/r/parents/{child_inscription_id}/inscriptions"
    ));

    assert_eq!(parent_inscriptions_json.parents.len(), 100);

    assert_eq!(
      parent_inscriptions_json.parents[0].id,
      first_parent_inscription_id
    );
    assert_eq!(parent_inscriptions_json.parents[0].number, 0); // parents are #0 and -1 to -110, child is #1

    assert_eq!(
      parent_inscriptions_json.parents[99].id,
      hundredth_parent_inscription_id
    );
    assert_eq!(parent_inscriptions_json.parents[99].number, -99); // all but 1st parent are cursed

    assert!(parent_inscriptions_json.more);
    assert_eq!(parent_inscriptions_json.page, 0);

    let parent_inscriptions_json = server.get_json::<api::ParentInscriptions>(format!(
      "/r/parents/{child_inscription_id}/inscriptions/1"
    ));

    assert_eq!(parent_inscriptions_json.parents.len(), 11);

    assert_eq!(
      parent_inscriptions_json.parents[0].id,
      hundred_first_parent_inscription_id
    );
    assert_eq!(parent_inscriptions_json.parents[0].number, -100);

    assert_eq!(
      parent_inscriptions_json.parents[10].id,
      hundred_eleventh_parent_inscription_id
    );
    assert_eq!(parent_inscriptions_json.parents[10].number, -110);

    assert!(!parent_inscriptions_json.more);
    assert_eq!(parent_inscriptions_json.page, 1);
  }

  #[test]
  fn inscriptions_in_block_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    for _ in 0..101 {
      server.mine_blocks(1);
    }

    for i in 0..101 {
      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/inscriptions/block/102",
      StatusCode::OK,
      r".*(<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>.*){100}.*",
    );

    server.assert_response_regex(
      "/inscriptions/block/102/1",
      StatusCode::OK,
      r".*<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>.*",
    );

    server.assert_response_regex(
      "/inscriptions/block/102/2",
      StatusCode::OK,
      r".*<div class=thumbnails>\s*</div>.*",
    );
  }

  #[test]
  fn inscription_query_display() {
    assert_eq!(
      query::Inscription::Id(inscription_id(1)).to_string(),
      "1111111111111111111111111111111111111111111111111111111111111111i1"
    );
    assert_eq!(query::Inscription::Number(1).to_string(), "1")
  }

  #[test]
  fn inscription_not_found() {
    TestServer::builder()
      .chain(Chain::Regtest)
      .build()
      .assert_response(
        "/inscription/0",
        StatusCode::NOT_FOUND,
        "inscription 0 not found",
      );
  }

  #[test]
  fn looking_up_inscription_by_sat_requires_sat_index() {
    TestServer::builder()
      .chain(Chain::Regtest)
      .build()
      .assert_response(
        "/inscription/abcd",
        StatusCode::NOT_FOUND,
        "sat index required",
      );
  }

  #[test]
  fn delegate() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let delegate = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, delegate.to_witness())],
      ..default()
    });

    let delegate = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    let inscription = Inscription {
      delegate: Some(delegate.value()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 1</h1>.*
        <dl>
          <dt>id</dt>
          <dd class=collapse>{id}</dd>
          .*
          <dt>delegate</dt>
          <dd><a href=/inscription/{delegate}>{delegate}</a></dd>
          .*
        </dl>.*"
      )
      .unindent(),
    );

    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");

    server.assert_response(format!("/preview/{id}"), StatusCode::OK, "foo");

    assert_eq!(
      server
        .get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}"))
        .delegate,
      Some(delegate)
    );
  }

  #[test]
  fn undelegated_content() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let delegate = Inscription {
      content_type: Some("text/plain".into()),
      body: Some("foo".into()),
      ..default()
    };

    let delegate_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, delegate.to_witness())],
      ..default()
    });

    let delegate_id = InscriptionId {
      txid: delegate_txid,
      index: 0,
    };

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/plain".into()),
      body: Some("bar".into()),
      delegate: Some(delegate_id.value()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response(
      format!("/r/undelegated-content/{id}"),
      StatusCode::OK,
      "bar",
    );

    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");

    // Test normal inscription without delegate
    let normal_inscription = Inscription {
      content_type: Some("text/plain".into()),
      body: Some("baz".into()),
      ..default()
    };

    let normal_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, normal_inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let normal_id = InscriptionId {
      txid: normal_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/undelegated-content/{normal_id}"),
      StatusCode::OK,
      "baz",
    );
    server.assert_response(format!("/content/{normal_id}"), StatusCode::OK, "baz");
  }

  #[test]
  fn content_proxy() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);

    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
    server_with_proxy.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
  }

  #[test]
  fn metadata_proxy() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let mut metadata = Vec::new();
    ciborium::into_writer("bar", &mut metadata).unwrap();

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      metadata: Some(metadata.clone()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response(
      format!("/r/metadata/{id}"),
      StatusCode::OK,
      &format!("\"{}\"", hex::encode(metadata.clone())),
    );

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);

    server.assert_response(
      format!("/r/metadata/{id}"),
      StatusCode::OK,
      &format!("\"{}\"", hex::encode(metadata.clone())),
    );

    server_with_proxy.assert_response(
      format!("/r/metadata/{id}"),
      StatusCode::OK,
      &format!("\"{}\"", hex::encode(metadata.clone())),
    );
  }

  #[test]
  fn children_proxy() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/children/{parent_id}"),
      StatusCode::NOT_FOUND,
      &format!("inscription {parent_id} not found"),
    );

    server.mine_blocks(1);

    let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));

    assert_eq!(children.ids.len(), 0);

    let mut builder = script::Builder::new();
    for _ in 0..11 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![parent_id.value()],
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    server.mine_blocks(1);

    let first_child_id = InscriptionId { txid, index: 0 };

    let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));

    assert_eq!(children.ids.len(), 11);
    assert_eq!(first_child_id, children.ids[0]);

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);

    let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));

    assert_eq!(children.ids.len(), 11);
    assert_eq!(first_child_id, children.ids[0]);

    let children = server_with_proxy.get_json::<api::Children>(format!("/r/children/{parent_id}"));

    assert_eq!(children.ids.len(), 11);
    assert_eq!(first_child_id, children.ids[0]);
  }

  #[test]
  fn inscription_proxy() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: Vec::new(),
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: Vec::new(),
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );

    assert_eq!(
      server_with_proxy.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: Vec::new(),
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );
  }

  #[test]
  fn sat_at_index_proxy() {
    let server = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };
    let ordinal: u64 = 5000000000;

    pretty_assert_eq!(
      server.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
      api::SatInscription { id: Some(id) }
    );

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();
    let sat_indexed_server_with_proxy = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);
    sat_indexed_server_with_proxy.mine_blocks(1);

    pretty_assert_eq!(
      server.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
      api::SatInscription { id: Some(id) }
    );

    pretty_assert_eq!(
      server_with_proxy.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
      api::SatInscription { id: Some(id) }
    );

    pretty_assert_eq!(
      sat_indexed_server_with_proxy
        .get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
      api::SatInscription { id: Some(id) }
    );
  }

  #[test]
  fn sat_at_index_content_proxy() {
    let server = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };
    let ordinal: u64 = 5000000000;

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: vec![Charm::Coin, Charm::Uncommon],
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: Some(Sat(ordinal)),
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );

    server.assert_response(
      format!("/r/sat/{ordinal}/at/-1/content"),
      StatusCode::OK,
      "foo",
    );

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();
    let sat_indexed_server_with_proxy = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);
    sat_indexed_server_with_proxy.mine_blocks(1);

    server.assert_response(
      format!("/r/sat/{ordinal}/at/-1/content"),
      StatusCode::OK,
      "foo",
    );
    server_with_proxy.assert_response(
      format!("/r/sat/{ordinal}/at/-1/content"),
      StatusCode::OK,
      "foo",
    );
    sat_indexed_server_with_proxy.assert_response(
      format!("/r/sat/{ordinal}/at/-1/content"),
      StatusCode::OK,
      "foo",
    );
  }

  #[test]
  fn block_info() {
    let server = TestServer::new();

    pretty_assert_eq!(
      server.get_json::<api::BlockInfo>("/r/blockinfo/0"),
      api::BlockInfo {
        average_fee: 0,
        average_fee_rate: 0,
        bits: 486604799,
        chainwork: [0; 32],
        confirmations: 0,
        difficulty: 0.0,
        hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
          .parse()
          .unwrap(),
        feerate_percentiles: [0, 0, 0, 0, 0],
        height: 0,
        max_fee: 0,
        max_fee_rate: 0,
        max_tx_size: 0,
        median_fee: 0,
        median_time: None,
        merkle_root: TxMerkleNode::all_zeros(),
        min_fee: 0,
        min_fee_rate: 0,
        next_block: None,
        nonce: 0,
        previous_block: None,
        subsidy: 0,
        target: "00000000ffff0000000000000000000000000000000000000000000000000000"
          .parse()
          .unwrap(),
        timestamp: 0,
        total_fee: 0,
        total_size: 0,
        total_weight: 0,
        transaction_count: 0,
        version: 1,
      },
    );

    server.mine_blocks(1);

    pretty_assert_eq!(
      server.get_json::<api::BlockInfo>("/r/blockinfo/1"),
      api::BlockInfo {
        average_fee: 0,
        average_fee_rate: 0,
        bits: 0,
        chainwork: [0; 32],
        confirmations: 0,
        difficulty: 0.0,
        hash: "56d05060a0280d0712d113f25321158747310ece87ea9e299bde06cf385b8d85"
          .parse()
          .unwrap(),
        feerate_percentiles: [0, 0, 0, 0, 0],
        height: 1,
        max_fee: 0,
        max_fee_rate: 0,
        max_tx_size: 0,
        median_fee: 0,
        median_time: None,
        merkle_root: TxMerkleNode::all_zeros(),
        min_fee: 0,
        min_fee_rate: 0,
        next_block: None,
        nonce: 0,
        previous_block: None,
        subsidy: 0,
        target: BlockHash::all_zeros(),
        timestamp: 0,
        total_fee: 0,
        total_size: 0,
        total_weight: 0,
        transaction_count: 0,
        version: 1,
      },
    )
  }

  #[test]
  fn authentication_requires_username_and_password() {
    assert!(Arguments::try_parse_from(["ord", "--server-username", "server", "foo"]).is_err());
    assert!(Arguments::try_parse_from(["ord", "--server-password", "server", "bar"]).is_err());
    assert!(
      Arguments::try_parse_from([
        "ord",
        "--server-username",
        "foo",
        "--server-password",
        "bar",
        "server"
      ])
      .is_ok()
    );
  }

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

    core.mine_blocks(1);

    let txid = core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    core.mine_blocks(1);

    let inscription = InscriptionId { txid, index: 0 };

    let server = TestServer::builder()
      .core(core)
      .config(&format!("hidden: [{inscription}]"))
      .build();

    server.assert_response_regex(format!("/inscription/{inscription}"), StatusCode::OK, ".*");

    server.assert_response_regex(
      format!("/content/{inscription}"),
      StatusCode::OK,
      PreviewUnknownHtml.to_string(),
    );
  }

  #[test]
  fn update_endpoint_is_not_available_when_not_in_integration_test_mode() {
    let server = TestServer::builder().build();
    server.assert_response("/update", StatusCode::NOT_FOUND, "");
  }

  #[test]
  fn burned_charm() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      outputs: 0,
      op_return_index: Some(0),
      op_return_value: Some(50 * COIN_VALUE),
      op_return: Some(
        script::Builder::new()
          .push_opcode(opcodes::all::OP_RETURN)
          .into_script(),
      ),
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: vec![Charm::Burned],
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: None
      }
    );
  }

  #[test]
  fn burned_charm_on_transfer() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let create_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      outputs: 1,
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId {
      txid: create_txid,
      index: 0,
    };

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: vec![],
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint {
          txid: create_txid,
          vout: 0
        },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint {
            txid: create_txid,
            vout: 0
          },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );

    let transfer_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 1, 0, Default::default())],
      fee: 0,
      outputs: 0,
      op_return_index: Some(0),
      op_return_value: Some(50 * COIN_VALUE),
      op_return: Some(
        script::Builder::new()
          .push_opcode(opcodes::all::OP_RETURN)
          .into_script(),
      ),
      ..default()
    });

    server.mine_blocks(1);

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: vec![Charm::Burned],
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint {
          txid: transfer_txid,
          vout: 0
        },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint {
            txid: transfer_txid,
            vout: 0
          },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: None
      }
    );
  }

  #[test]
  fn unknown_output_returns_404() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.assert_response(
      "/output/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123",
      StatusCode::NOT_FOUND,
      "output 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123 not found",
    );
  }

  #[test]
  fn satscard_form_with_coinkite_url_redirects_to_query() {
    TestServer::new().assert_redirect(
      &format!(
        "/satscard?url={}",
        urlencoding::encode(satscard::tests::COINKITE_URL)
      ),
      &format!("/satscard?{}", satscard::tests::coinkite_fragment()),
    );
  }

  #[test]
  fn satscard_form_with_ordinals_url_redirects_to_query() {
    TestServer::new().assert_redirect(
      &format!(
        "/satscard?url={}",
        urlencoding::encode(satscard::tests::ORDINALS_URL)
      ),
      &format!("/satscard?{}", satscard::tests::ordinals_query()),
    );
  }

  #[test]
  fn satscard_missing_form_query_is_error() {
    TestServer::new().assert_response(
      "/satscard?url=https://foo.com",
      StatusCode::BAD_REQUEST,
      "satscard URL missing fragment",
    );
  }

  #[test]
  fn satscard_invalid_query_parameters() {
    TestServer::new().assert_response(
      "/satscard?foo=bar",
      StatusCode::BAD_REQUEST,
      "invalid satscard query parameters: unknown key `foo`",
    );
  }

  #[test]
  fn satscard_empty_query_parameters_are_allowed() {
    TestServer::builder()
      .chain(Chain::Mainnet)
      .build()
      .assert_html("/satscard?", SatscardHtml { satscard: None });
  }

  #[test]
  fn satscard_display_without_address_index() {
    TestServer::builder()
      .chain(Chain::Mainnet)
      .build()
      .assert_html(
        format!("/satscard?{}", satscard::tests::coinkite_fragment()),
        SatscardHtml {
          satscard: Some((satscard::tests::coinkite_satscard(), None)),
        },
      );
  }

  #[test]
  fn satscard_coinkite_display_with_address_index_empty() {
    TestServer::builder()
      .chain(Chain::Mainnet)
      .index_addresses()
      .build()
      .assert_html(
        format!("/satscard?{}", satscard::tests::coinkite_fragment()),
        SatscardHtml {
          satscard: Some((
            satscard::tests::coinkite_satscard(),
            Some(AddressHtml {
              address: satscard::tests::coinkite_address(),
              header: false,
              inscriptions: Some(Vec::new()),
              outputs: Vec::new(),
              runes_balances: None,
              sat_balance: 0,
            }),
          )),
        },
      );
  }

  #[test]
  fn satscard_ordinals_display_with_address_index_empty() {
    TestServer::builder()
      .chain(Chain::Mainnet)
      .index_addresses()
      .build()
      .assert_html(
        format!("/satscard?{}", satscard::tests::ordinals_query()),
        SatscardHtml {
          satscard: Some((
            satscard::tests::ordinals_satscard(),
            Some(AddressHtml {
              address: satscard::tests::ordinals_address(),
              header: false,
              inscriptions: Some(Vec::new()),
              outputs: Vec::new(),
              runes_balances: None,
              sat_balance: 0,
            }),
          )),
        },
      );
  }

  #[test]
  fn satscard_address_recovery_fails_on_wrong_chain() {
    TestServer::builder()
      .chain(Chain::Testnet)
      .build()
      .assert_response(
        format!("/satscard?{}", satscard::tests::coinkite_fragment()),
        StatusCode::BAD_REQUEST,
        "invalid satscard query parameters: address recovery failed",
      );
  }

  #[test]
  fn sat_inscription_at_index_content_endpoint() {
    let server = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .build();

    server.mine_blocks(1);

    let first_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/plain;charset=utf-8", "foo").to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    let first_inscription_id = InscriptionId {
      txid: first_txid,
      index: 0,
    };

    let first_inscription = server
      .get_json::<api::InscriptionRecursive>(format!("/r/inscription/{first_inscription_id}"));

    let sat = first_inscription.sat.unwrap();

    server.assert_response(format!("/r/sat/{sat}/at/0/content"), StatusCode::OK, "foo");

    server.assert_response(format!("/r/sat/{sat}/at/-1/content"), StatusCode::OK, "foo");

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        2,
        1,
        first_inscription.satpoint.outpoint.vout.try_into().unwrap(),
        inscription("text/plain;charset=utf-8", "bar").to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response(format!("/r/sat/{sat}/at/0/content"), StatusCode::OK, "foo");

    server.assert_response(format!("/r/sat/{sat}/at/1/content"), StatusCode::OK, "bar");

    server.assert_response(format!("/r/sat/{sat}/at/-1/content"), StatusCode::OK, "bar");

    server.assert_response(
      "/r/sat/0/at/0/content",
      StatusCode::NOT_FOUND,
      "inscription on sat 0 not found",
    );

    let server = TestServer::new();

    server.assert_response(
      "/r/sat/0/at/0/content",
      StatusCode::NOT_FOUND,
      "this server has no sat index",
    );
  }

  #[test]
  fn offers_are_accepted() {
    let server = TestServer::builder().server_flag("--accept-offers").build();

    let psbt0 = base64_encode(
      &Psbt {
        unsigned_tx: Transaction {
          version: Version(0),
          lock_time: LockTime::ZERO,
          input: Vec::new(),
          output: Vec::new(),
        },
        version: 0,
        xpub: BTreeMap::new(),
        proprietary: BTreeMap::new(),
        unknown: BTreeMap::new(),
        inputs: Vec::new(),
        outputs: Vec::new(),
      }
      .serialize(),
    );

    let response = server.post("offer", &psbt0, StatusCode::OK);

    assert_eq!(response.text().unwrap(), "");

    let offers = server.get_json::<api::Offers>("/offers");

    assert_eq!(
      offers,
      api::Offers {
        offers: vec![psbt0.clone()],
      },
    );

    let psbt1 = base64_encode(
      &Psbt {
        unsigned_tx: Transaction {
          version: Version(1),
          lock_time: LockTime::ZERO,
          input: Vec::new(),
          output: Vec::new(),
        },
        version: 0,
        xpub: BTreeMap::new(),
        proprietary: BTreeMap::new(),
        unknown: BTreeMap::new(),
        inputs: Vec::new(),
        outputs: Vec::new(),
      }
      .serialize(),
    );

    let response = server.post("offer", &psbt1, StatusCode::OK);

    assert_eq!(response.text().unwrap(), "");

    let offers = server.get_json::<api::Offers>("/offers");

    assert_eq!(
      offers,
      api::Offers {
        offers: vec![psbt0, psbt1],
      },
    );
  }

  #[test]
  fn offers_are_rejected_if_not_valid_psbts() {
    let server = TestServer::builder().server_flag("--accept-offers").build();
    server.post("offer", "0", StatusCode::BAD_REQUEST);
  }

  #[test]
  fn offer_acceptance_requires_accept_offers_flag() {
    let server = TestServer::builder().build();

    let psbt = base64_encode(
      &Psbt {
        unsigned_tx: Transaction {
          version: Version(0),
          lock_time: LockTime::ZERO,
          input: Vec::new(),
          output: Vec::new(),
        },
        version: 0,
        xpub: BTreeMap::new(),
        proprietary: BTreeMap::new(),
        unknown: BTreeMap::new(),
        inputs: Vec::new(),
        outputs: Vec::new(),
      }
      .serialize(),
    );

    server.post("offer", &psbt, StatusCode::NOT_FOUND);
  }

  #[test]
  fn offer_acceptance_does_not_require_json_api() {
    let server = TestServer::builder()
      .server_flag("--disable-json-api")
      .server_flag("--accept-offers")
      .build();

    let psbt = base64_encode(
      &Psbt {
        unsigned_tx: Transaction {
          version: Version(0),
          lock_time: LockTime::ZERO,
          input: Vec::new(),
          output: Vec::new(),
        },
        version: 0,
        xpub: BTreeMap::new(),
        proprietary: BTreeMap::new(),
        unknown: BTreeMap::new(),
        inputs: Vec::new(),
        outputs: Vec::new(),
      }
      .serialize(),
    );

    server.post("offer", &psbt, StatusCode::OK);
  }

  #[test]
  fn offer_size_is_limited() {
    let server = TestServer::builder().server_flag("--accept-offers").build();

    let psbt = base64_encode(
      &Psbt {
        unsigned_tx: Transaction {
          version: Version(0),
          lock_time: LockTime::ZERO,
          input: Vec::new(),
          output: vec![TxOut {
            value: Amount::from_sat(1),
            script_pubkey: ScriptBuf::builder()
              .push_slice::<&PushBytes>(vec![0; 2 * MEBIBYTE].as_slice().try_into().unwrap())
              .into_script(),
          }],
        },
        version: 0,
        xpub: BTreeMap::new(),
        proprietary: BTreeMap::new(),
        unknown: BTreeMap::new(),
        inputs: Vec::new(),
        outputs: vec![bitcoin::psbt::Output::default()],
      }
      .serialize(),
    );

    server.post("offer", &psbt, StatusCode::PAYLOAD_TOO_LARGE);
  }

  #[test]
  fn missing_returns_missing_inscription_ids() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let existing = InscriptionId { txid, index: 0 };

    let missing_id = "0000000000000000000000000000000000000000000000000000000000000000i0"
      .parse::<InscriptionId>()
      .unwrap();

    let result = server.post_json::<Vec<InscriptionId>>("missing", &vec![existing, missing_id]);

    assert_eq!(result, vec![missing_id]);
  }

  #[test]
  fn missing_returns_empty_when_all_exist() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let existing = InscriptionId { txid, index: 0 };

    let result = server.post_json::<Vec<InscriptionId>>("missing", &vec![existing]);

    assert!(result.is_empty());
  }

  #[test]
  fn post_bodies_are_limited_when_json_api_is_disabled() {
    let server = TestServer::builder()
      .server_flag("--disable-json-api")
      .build();

    let client = reqwest::blocking::Client::new();

    let response = client
      .post(server.join_url("outputs"))
      .header(header::CONTENT_TYPE, "application/json")
      .body(" ".repeat(2 * MEBIBYTE + 1))
      .send()
      .unwrap();

    assert_eq!(
      response.status(),
      StatusCode::PAYLOAD_TOO_LARGE,
      "{}",
      response.text().unwrap(),
    );

    let response = client
      .post(server.join_url("inscriptions"))
      .header(header::CONTENT_TYPE, "application/json")
      .body(" ".repeat(2 * MEBIBYTE + 1))
      .send()
      .unwrap();

    assert_eq!(
      response.status(),
      StatusCode::PAYLOAD_TOO_LARGE,
      "{}",
      response.text().unwrap(),
    );
  }
}
