use {
  super::*,
  anyhow::ensure,
  axum::http::header::HeaderValue,
  bitcoin::blockdata::opcodes,
  brotli::enc::{
    BrotliEncoderParams, backward_references::BrotliEncoderMode, writer::CompressorWriter,
  },
  io::Write,
};

const MAX_COMPRESSED_PROPERTIES_SIZE: usize = 4_000_000;
const MAX_PROPERTIES_COMPRESSION_RATIO: usize = 30;

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq, Default)]
pub struct Inscription {
  pub body: Option<Vec<u8>>,
  pub content_encoding: Option<Vec<u8>>,
  pub content_type: Option<Vec<u8>>,
  pub delegate: Option<Vec<u8>>,
  pub duplicate_field: bool,
  pub incomplete_field: bool,
  pub metadata: Option<Vec<u8>>,
  pub metaprotocol: Option<Vec<u8>>,
  pub parents: Vec<Vec<u8>>,
  pub pointer: Option<Vec<u8>>,
  pub properties: Option<Vec<u8>>,
  pub property_encoding: Option<Vec<u8>>,
  pub rune: Option<Vec<u8>>,
  pub unrecognized_even_field: bool,
}

impl Inscription {
  pub fn new(
    chain: Chain,
    compress: bool,
    delegate: Option<InscriptionId>,
    metadata: Option<Vec<u8>>,
    metaprotocol: Option<String>,
    parents: Vec<InscriptionId>,
    path: Option<PathBuf>,
    pointer: Option<u64>,
    properties: Properties,
    rune: Option<Rune>,
  ) -> Result<Self, Error> {
    let path = path.as_ref();

    let (body, content_type, content_encoding) = if let Some(path) = path {
      let body = fs::read(path).with_context(|| format!("io error reading {}", path.display()))?;

      let content_type = Media::content_type_for_path(path)?.0;

      let (body, content_encoding) = if compress {
        let mode = Media::content_type_for_path(path)?.1;
        Self::compress(mode, body)?
      } else {
        (body, None)
      };

      if let Some(limit) = chain.inscription_content_size_limit() {
        let len = body.len();
        if len > limit {
          bail!("content size of {len} bytes exceeds {limit} byte limit for {chain} inscriptions");
        }
      }

      (Some(body), Some(content_type), content_encoding)
    } else {
      (None, None, None)
    };

    let (properties, property_encoding) = Self::encode_properties(compress, &properties)?;

    Ok(Self {
      body,
      content_encoding,
      content_type: content_type.map(|content_type| content_type.into()),
      delegate: delegate.map(|delegate| delegate.value()),
      duplicate_field: false,
      incomplete_field: false,
      metadata,
      metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()),
      parents: parents.iter().map(|parent| parent.value()).collect(),
      pointer: pointer.map(Self::pointer_value),
      property_encoding,
      properties,
      rune: rune.map(|rune| rune.commitment()),
      unrecognized_even_field: false,
    })
  }

  pub fn pointer_value(pointer: u64) -> Vec<u8> {
    let mut bytes = pointer.to_le_bytes().to_vec();

    while bytes.last().copied() == Some(0) {
      bytes.pop();
    }

    bytes
  }

  fn encode_properties(
    compress: bool,
    properties: &Properties,
  ) -> Result<(Option<Vec<u8>>, Option<Vec<u8>>)> {
    let Some(inline) = properties.to_inline_cbor() else {
      return Ok((None, None));
    };

    let packed = properties.to_packed_cbor().unwrap();

    let mut candidates = vec![(inline.clone(), None), (packed.clone(), None)];

    if compress {
      if let (cbor, Some(encoding)) = Self::compress_properties(inline)? {
        candidates.push((cbor, Some(encoding)))
      }

      if let (cbor, Some(encoding)) = Self::compress_properties(packed)? {
        candidates.push((cbor, Some(encoding)))
      }
    }

    let (bytes, encoding) = candidates
      .into_iter()
      .min_by_key(|(bytes, _)| bytes.len())
      .unwrap();

    Ok((Some(bytes), encoding))
  }

  fn compress_properties(cbor: Vec<u8>) -> Result<(Vec<u8>, Option<Vec<u8>>)> {
    let len = cbor.len();

    ensure! {
      len <= MAX_COMPRESSED_PROPERTIES_SIZE,
      "properties size of {len} bytes exceeds {MAX_COMPRESSED_PROPERTIES_SIZE} byte limit",
    }

    let (compressed, encoding) = Self::compress(BrotliEncoderMode::BROTLI_MODE_GENERIC, cbor)?;

    if encoding.is_some() {
      ensure! {
        len / compressed.len() <= MAX_PROPERTIES_COMPRESSION_RATIO,
        "property compression over {MAX_PROPERTIES_COMPRESSION_RATIO}:1",
      }
    }

    Ok((compressed, encoding))
  }

  fn compress(mode: BrotliEncoderMode, data: Vec<u8>) -> Result<(Vec<u8>, Option<Vec<u8>>), Error> {
    let mut compressor = CompressorWriter::with_params(
      Vec::new(),
      data.len(),
      &BrotliEncoderParams {
        lgblock: 24,
        lgwin: 24,
        mode,
        quality: 11,
        size_hint: data.len(),
        ..default()
      },
    );

    compressor.write_all(&data)?;

    let compressed = compressor.into_inner();

    let mut decompressor = brotli::Decompressor::new(compressed.as_slice(), compressed.len());

    let mut decompressed = Vec::new();

    decompressor.read_to_end(&mut decompressed)?;

    assert!(decompressed == data, "decompression roundtrip failed");

    if compressed.len() < data.len() {
      Ok((compressed, Some(BROTLI.as_bytes().into())))
    } else {
      Ok((data, None))
    }
  }

  pub fn append_reveal_script_to_builder(&self, mut builder: script::Builder) -> script::Builder {
    builder = builder
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(envelope::PROTOCOL_ID);

    Tag::ContentType.append(&mut builder, &self.content_type);
    Tag::ContentEncoding.append(&mut builder, &self.content_encoding);
    Tag::Metaprotocol.append(&mut builder, &self.metaprotocol);
    Tag::Parent.append_array(&mut builder, &self.parents);
    Tag::Delegate.append(&mut builder, &self.delegate);
    Tag::Pointer.append(&mut builder, &self.pointer);
    Tag::Metadata.append(&mut builder, &self.metadata);
    Tag::Rune.append(&mut builder, &self.rune);
    Tag::Properties.append(&mut builder, &self.properties);
    Tag::PropertyEncoding.append(&mut builder, &self.property_encoding);

    if let Some(body) = &self.body {
      builder = builder.push_slice(envelope::BODY_TAG);
      for chunk in body.chunks(MAX_SCRIPT_ELEMENT_SIZE) {
        builder = builder.push_slice::<&script::PushBytes>(chunk.try_into().unwrap());
      }
    }

    builder.push_opcode(opcodes::all::OP_ENDIF)
  }

  #[cfg(test)]
  pub(crate) fn append_reveal_script(&self, builder: script::Builder) -> ScriptBuf {
    self.append_reveal_script_to_builder(builder).into_script()
  }

  pub fn append_batch_reveal_script_to_builder(
    inscriptions: &[Inscription],
    mut builder: script::Builder,
  ) -> script::Builder {
    for inscription in inscriptions {
      builder = inscription.append_reveal_script_to_builder(builder);
    }

    builder
  }

  pub fn append_batch_reveal_script(
    inscriptions: &[Inscription],
    builder: script::Builder,
  ) -> ScriptBuf {
    Inscription::append_batch_reveal_script_to_builder(inscriptions, builder).into_script()
  }

  pub fn media(&self) -> Media {
    if self.body.is_none() {
      return Media::Unknown;
    }

    let Some(content_type) = self.content_type() else {
      return Media::Unknown;
    };

    content_type.parse().unwrap_or(Media::Unknown)
  }

  pub fn body(&self) -> Option<&[u8]> {
    Some(self.body.as_ref()?)
  }

  pub fn into_body(self) -> Option<Vec<u8>> {
    self.body
  }

  pub fn content_length(&self) -> Option<usize> {
    Some(self.body()?.len())
  }

  pub fn content_type(&self) -> Option<&str> {
    str::from_utf8(self.content_type.as_ref()?).ok()
  }

  pub fn content_encoding(&self) -> Option<HeaderValue> {
    HeaderValue::from_str(str::from_utf8(self.content_encoding.as_ref()?).unwrap_or_default()).ok()
  }

  pub fn delegate(&self) -> Option<InscriptionId> {
    InscriptionId::from_value(self.delegate.as_deref()?)
  }

  pub fn metadata(&self) -> Option<Value> {
    ciborium::from_reader(Cursor::new(self.metadata.as_ref()?)).ok()
  }

  pub fn metaprotocol(&self) -> Option<&str> {
    str::from_utf8(self.metaprotocol.as_ref()?).ok()
  }

  pub fn parents(&self) -> Vec<InscriptionId> {
    self
      .parents
      .iter()
      .filter_map(|parent| InscriptionId::from_value(parent))
      .collect()
  }

  pub fn pointer(&self) -> Option<u64> {
    let value = self.pointer.as_ref()?;

    if value.iter().skip(8).copied().any(|byte| byte != 0) {
      return None;
    }

    let pointer = [
      value.first().copied().unwrap_or(0),
      value.get(1).copied().unwrap_or(0),
      value.get(2).copied().unwrap_or(0),
      value.get(3).copied().unwrap_or(0),
      value.get(4).copied().unwrap_or(0),
      value.get(5).copied().unwrap_or(0),
      value.get(6).copied().unwrap_or(0),
      value.get(7).copied().unwrap_or(0),
    ];

    Some(u64::from_le_bytes(pointer))
  }

  #[cfg(test)]
  pub(crate) fn to_witness(&self) -> Witness {
    let builder = script::Builder::new();

    let script = self.append_reveal_script(builder);

    let mut witness = Witness::new();

    witness.push(script);
    witness.push([]);

    witness
  }

  pub fn hidden(&self) -> bool {
    use regex::bytes::Regex;

    const BVM_NETWORK: &[u8] = b"<body style=\"background:#F61;color:#fff;\">\
                        <h1 style=\"height:100%\">bvm.network</h1></body>";

    static BRC_420: LazyLock<Regex> =
      LazyLock::new(|| Regex::new(r"^\s*/content/[[:xdigit:]]{64}i\d+\s*$").unwrap());

    self
      .body()
      .map(|body| BRC_420.is_match(body) || body.starts_with(BVM_NETWORK))
      .unwrap_or_default()
      || self.metaprotocol.is_some()
      || matches!(self.media(), Media::Code(_) | Media::Text | Media::Unknown)
  }

  pub(crate) fn properties(&self) -> Properties {
    self
      .properties_cbor()
      .map(|cbor| Properties::from_cbor(&cbor))
      .unwrap_or_default()
  }

  fn properties_cbor(&self) -> Option<Cow<[u8]>> {
    let value = self.properties.as_deref()?;

    if let Some(encoding) = &self.property_encoding {
      if encoding != BROTLI.as_bytes() {
        return None;
      }

      let max = value
        .len()
        .saturating_mul(MAX_PROPERTIES_COMPRESSION_RATIO)
        .min(MAX_COMPRESSED_PROPERTIES_SIZE);

      let mut decompressor = brotli::Decompressor::new(value, BROTLI_BUFFER_SIZE);

      let mut value = Vec::new();

      let mut buffer = vec![0; BROTLI_BUFFER_SIZE];

      loop {
        let n = decompressor.read(&mut buffer).ok()?;

        if n == 0 {
          break;
        }

        if value.len() + n > max {
          return None;
        }

        value.extend_from_slice(&buffer[..n]);
      }

      Some(Cow::Owned(value))
    } else {
      Some(Cow::Borrowed(value))
    }
  }
}

#[cfg(test)]
mod tests {
  use {super::*, std::io::Write};

  #[test]
  fn reveal_script_chunks_body() {
    assert_eq!(
      inscription("foo", [])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      7
    );

    assert_eq!(
      inscription("foo", [0; 1])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      8
    );

    assert_eq!(
      inscription("foo", [0; 520])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      8
    );

    assert_eq!(
      inscription("foo", [0; 521])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      9
    );

    assert_eq!(
      inscription("foo", [0; 1040])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      9
    );

    assert_eq!(
      inscription("foo", [0; 1041])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      10
    );
  }

  #[test]
  fn reveal_script_chunks_metadata() {
    assert_eq!(
      Inscription {
        metadata: None,
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      4
    );

    assert_eq!(
      Inscription {
        metadata: Some(Vec::new()),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      4
    );

    assert_eq!(
      Inscription {
        metadata: Some(vec![0; 1]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      6
    );

    assert_eq!(
      Inscription {
        metadata: Some(vec![0; 520]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      6
    );

    assert_eq!(
      Inscription {
        metadata: Some(vec![0; 521]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      8
    );
  }

  #[test]
  fn reveal_script_chunks_properties() {
    assert_eq!(
      Inscription {
        properties: None,
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      4
    );

    assert_eq!(
      Inscription {
        properties: Some(Vec::new()),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      4
    );

    assert_eq!(
      Inscription {
        properties: Some(vec![0; 1]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      6
    );

    assert_eq!(
      Inscription {
        properties: Some(vec![0; 520]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      6
    );

    assert_eq!(
      Inscription {
        properties: Some(vec![0; 521]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      8
    );
  }

  #[test]
  fn inscription_with_no_parent_field_has_no_parent() {
    assert!(
      Inscription {
        parents: Vec::new(),
        ..default()
      }
      .parents()
      .is_empty()
    );
  }

  #[test]
  fn inscription_with_parent_field_shorter_than_txid_length_has_no_parent() {
    assert!(
      Inscription {
        parents: vec![Vec::new()],
        ..default()
      }
      .parents()
      .is_empty()
    );
  }

  #[test]
  fn inscription_with_parent_field_longer_than_txid_and_index_has_no_parent() {
    assert!(
      Inscription {
        parents: vec![vec![1; 37]],
        ..default()
      }
      .parents()
      .is_empty()
    );
  }

  #[test]
  fn inscription_with_parent_field_index_with_trailing_zeroes_and_fixed_length_has_parent() {
    let mut parent = vec![1; 36];

    parent[35] = 0;

    assert!(
      !Inscription {
        parents: vec![parent],
        ..default()
      }
      .parents()
      .is_empty()
    );
  }

  #[test]
  fn inscription_with_parent_field_index_with_trailing_zeroes_and_variable_length_has_no_parent() {
    let mut parent = vec![1; 35];

    parent[34] = 0;

    assert!(
      Inscription {
        parents: vec![parent],
        ..default()
      }
      .parents()
      .is_empty()
    );
  }

  #[test]
  fn inscription_delegate_txid_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        delegate: Some(vec![
          0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
          0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
          0x1e, 0x1f,
        ]),
        ..default()
      }
      .delegate()
      .unwrap()
      .txid,
      "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"
        .parse()
        .unwrap()
    );
  }

  #[test]
  fn inscription_parent_txid_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
          0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
          0x1e, 0x1f,
        ]],
        ..default()
      }
      .parents(),
      [
        "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100i0"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_zero_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![1; 32]],
        ..default()
      }
      .parents(),
      [
        "0101010101010101010101010101010101010101010101010101010101010101i0"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_one_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0x01
        ]],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi1"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_two_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0x01, 0x02
        ]],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi513"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_three_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0x01, 0x02, 0x03
        ]],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi197121"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_four_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0x01, 0x02, 0x03, 0x04,
        ]],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305985"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_returns_multiple_parents() {
    assert_eq!(
      Inscription {
        parents: vec![
          vec![
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03, 0x04,
          ],
          vec![
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0x00, 0x02, 0x03, 0x04,
          ]
        ],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305985"
          .parse()
          .unwrap(),
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305984"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn metadata_function_decodes_metadata() {
    assert_eq!(
      Inscription {
        metadata: Some(vec![0x44, 0, 1, 2, 3]),
        ..default()
      }
      .metadata()
      .unwrap(),
      Value::Bytes(vec![0, 1, 2, 3]),
    );
  }

  #[test]
  fn metadata_function_returns_none_if_no_metadata() {
    assert_eq!(
      Inscription {
        metadata: None,
        ..default()
      }
      .metadata(),
      None,
    );
  }

  #[test]
  fn metadata_function_returns_none_if_metadata_fails_to_parse() {
    assert_eq!(
      Inscription {
        metadata: Some(vec![0x44]),
        ..default()
      }
      .metadata(),
      None,
    );
  }

  #[test]
  fn pointer_decode() {
    assert_eq!(
      Inscription {
        pointer: None,
        ..default()
      }
      .pointer(),
      None
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![0]),
        ..default()
      }
      .pointer(),
      Some(0),
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8]),
        ..default()
      }
      .pointer(),
      Some(0x0807060504030201),
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6]),
        ..default()
      }
      .pointer(),
      Some(0x0000060504030201),
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0]),
        ..default()
      }
      .pointer(),
      Some(0x0807060504030201),
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 1]),
        ..default()
      }
      .pointer(),
      None,
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 1]),
        ..default()
      }
      .pointer(),
      None,
    );
  }

  #[test]
  fn pointer_encode() {
    assert_eq!(
      Inscription {
        pointer: None,
        ..default()
      }
      .to_witness(),
      envelope(&[b"ord"]),
    );

    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3]),
        ..default()
      }
      .to_witness(),
      envelope(&[b"ord", &[2], &[1, 2, 3]]),
    );
  }

  #[test]
  fn pointer_value() {
    let mut file = tempfile::Builder::new().suffix(".txt").tempfile().unwrap();

    write!(file, "foo").unwrap();

    let inscription = Inscription::new(
      Chain::Mainnet,
      false,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      None,
      Properties::default(),
      None,
    )
    .unwrap();

    assert_eq!(inscription.pointer, None);

    let inscription = Inscription::new(
      Chain::Mainnet,
      false,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      Some(0),
      Properties::default(),
      None,
    )
    .unwrap();

    assert_eq!(inscription.pointer, Some(Vec::new()));

    let inscription = Inscription::new(
      Chain::Mainnet,
      false,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      Some(1),
      Properties::default(),
      None,
    )
    .unwrap();

    assert_eq!(inscription.pointer, Some(vec![1]));

    let inscription = Inscription::new(
      Chain::Mainnet,
      false,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      Some(256),
      Properties::default(),
      None,
    )
    .unwrap();

    assert_eq!(inscription.pointer, Some(vec![0, 1]));
  }

  #[test]
  fn hidden() {
    #[track_caller]
    fn case(content_type: Option<&str>, body: Option<&str>, expected: bool) {
      assert_eq!(
        Inscription {
          content_type: content_type.map(|content_type| content_type.as_bytes().into()),
          body: body.map(|content_type| content_type.as_bytes().into()),
          ..default()
        }
        .hidden(),
        expected
      );
    }

    case(None, None, true);
    case(Some("foo"), Some(""), true);
    case(Some("text/plain"), None, true);
    case(
      Some("text/plain"),
      Some("The fox jumped. The cow danced."),
      true,
    );
    case(Some("text/plain;charset=utf-8"), Some("foo"), true);
    case(Some("text/plain;charset=cn-big5"), Some("foo"), true);
    case(Some("application/json"), Some("foo"), true);
    case(
      Some("text/markdown"),
      Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"),
      true,
    );
    case(
      Some("text/html"),
      Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"),
      true,
    );
    case(Some("application/yaml"), Some(""), true);
    case(
      Some("text/html;charset=utf-8"),
      Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"),
      true,
    );
    case(
      Some("text/html"),
      Some("  /content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0  \n"),
      true,
    );
    case(
      Some("text/html"),
      Some(
        r#"<body style="background:#F61;color:#fff;"><h1 style="height:100%">bvm.network</h1></body>"#,
      ),
      true,
    );
    case(
      Some("text/html"),
      Some(
        r#"<body style="background:#F61;color:#fff;"><h1 style="height:100%">bvm.network</h1></body>foo"#,
      ),
      true,
    );

    assert!(
      Inscription {
        content_type: Some("text/plain".as_bytes().into()),
        body: Some(b"{\xc3\x28}".as_slice().into()),
        ..default()
      }
      .hidden()
    );

    assert!(
      Inscription {
        content_type: Some("text/html".as_bytes().into()),
        body: Some("hello".as_bytes().into()),
        metaprotocol: Some(Vec::new()),
        ..default()
      }
      .hidden()
    );
  }

  #[test]
  fn properties_cbor_without_properties() {
    assert!(
      Inscription {
        properties: None,
        ..default()
      }
      .properties_cbor()
      .is_none()
    );
  }

  #[test]
  fn properties_cbor_uncompressed() {
    let cbor = minicbor::to_vec(Properties {
      attributes: Attributes {
        title: Some("foo".into()),
        ..default()
      },
      ..default()
    })
    .unwrap();

    assert_eq!(
      Inscription {
        properties: Some(cbor.clone()),
        ..default()
      }
      .properties_cbor()
      .unwrap()
      .into_owned(),
      cbor,
    );
  }

  #[test]
  fn properties_cbor_compressed() {
    let cbor = minicbor::to_vec(Properties {
      attributes: Attributes {
        title: Some("foo".into()),
        ..default()
      },
      ..default()
    })
    .unwrap();

    let mut compressed = Vec::new();

    CompressorWriter::new(&mut compressed, BROTLI_BUFFER_SIZE, 11, 22)
      .write_all(&cbor)
      .unwrap();

    assert_eq!(
      Inscription {
        properties: Some(compressed),
        property_encoding: Some(BROTLI.into()),
        ..default()
      }
      .properties_cbor()
      .unwrap()
      .into_owned(),
      cbor,
    );
  }

  #[test]
  fn properties_cbor_unknown_encoding() {
    let cbor = minicbor::to_vec(Properties::default()).unwrap();

    assert!(
      Inscription {
        properties: Some(cbor),
        property_encoding: Some("foo".into()),
        ..default()
      }
      .properties_cbor()
      .is_none()
    );
  }

  #[test]
  fn properties_cbor_invalid_brotli() {
    assert!(
      Inscription {
        properties: Some(vec![0, 1, 2, 3]),
        property_encoding: Some(BROTLI.into()),
        ..default()
      }
      .properties_cbor()
      .is_none()
    );
  }

  #[test]
  fn properties_cbor_exceeds_limit() {
    let cbor = vec![0u8; 4_000_001];

    let mut compressed = Vec::new();

    CompressorWriter::new(&mut compressed, BROTLI_BUFFER_SIZE, 11, 22)
      .write_all(&cbor)
      .unwrap();

    assert!(
      Inscription {
        properties: Some(compressed),
        property_encoding: Some(BROTLI.into()),
        ..default()
      }
      .properties_cbor()
      .is_none()
    );
  }

  #[test]
  fn new_compresses_properties() {
    let mut file = tempfile::Builder::new().suffix(".txt").tempfile().unwrap();

    write!(file, "foo").unwrap();

    let properties = Properties {
      attributes: Attributes {
        title: Some("a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]a]".into()),
        ..default()
      },
      ..default()
    };

    let cbor = properties.to_inline_cbor().unwrap();

    let inscription = Inscription::new(
      Chain::Mainnet,
      true,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      None,
      properties.clone(),
      None,
    )
    .unwrap();

    assert!(inscription.properties.as_ref().unwrap().len() < cbor.len());
    assert_eq!(inscription.property_encoding, Some(BROTLI.into()));
    assert_eq!(inscription.properties(), properties);
  }

  #[test]
  fn new_rejects_oversized_properties() {
    let mut file = tempfile::Builder::new().suffix(".txt").tempfile().unwrap();

    write!(file, "foo").unwrap();

    let properties = Properties {
      attributes: Attributes {
        title: Some("x".repeat(4_000_001)),
        ..default()
      },
      ..default()
    };

    assert!(
      Inscription::new(
        Chain::Mainnet,
        true,
        None,
        None,
        None,
        Vec::new(),
        Some(file.path().to_path_buf()),
        None,
        properties,
        None,
      )
      .unwrap_err()
      .to_string()
      .contains("exceeds 4000000 byte limit")
    );
  }

  #[test]
  fn new_rejects_high_compression_ratio_properties() {
    let mut file = tempfile::Builder::new().suffix(".txt").tempfile().unwrap();

    write!(file, "foo").unwrap();

    let properties = Properties {
      attributes: Attributes {
        title: Some("a".repeat(10_000)),
        ..default()
      },
      ..default()
    };

    assert!(
      Inscription::new(
        Chain::Mainnet,
        true,
        None,
        None,
        None,
        Vec::new(),
        Some(file.path().to_path_buf()),
        None,
        properties,
        None,
      )
      .unwrap_err()
      .to_string()
      .contains("compression over 30:1")
    );
  }

  #[test]
  fn encode_properties_selects_smallest_candidate() {
    fn item(txid: Txid, index: u32) -> properties::Item {
      properties::Item {
        id: Some(InscriptionId { txid, index }),
        attributes: Attributes::default(),
        index: None,
      }
    }

    fn item_with_attributes(txid: Txid, index: u32, title: &str) -> properties::Item {
      properties::Item {
        id: Some(InscriptionId { txid, index }),
        attributes: Attributes {
          title: Some(title.into()),
          ..default()
        },
        index: None,
      }
    }

    let txid_a = inscription_id(0).txid;

    // inline uncompressed: single item with non-zero index,
    // packed adds overhead for separate index field and txid blob
    {
      let properties = Properties {
        gallery: vec![item(txid_a, 5)],
        ..default()
      };

      let (bytes, encoding) = Inscription::encode_properties(false, &properties).unwrap();
      let bytes = bytes.unwrap();

      assert!(encoding.is_none());
      assert_eq!(bytes, properties.to_inline_cbor().unwrap());
    }

    // packed uncompressed: many items with index 0,
    // packed saves per-item overhead
    {
      let properties = Properties {
        gallery: (0..10)
          .map(|i| {
            let mut txid = [0u8; 32];
            txid[0] = i;
            item(Txid::from_byte_array(txid), 0)
          })
          .collect(),
        ..default()
      };

      let (bytes, encoding) = Inscription::encode_properties(false, &properties).unwrap();
      let bytes = bytes.unwrap();

      assert!(encoding.is_none());
      assert_eq!(bytes, properties.to_packed_cbor().unwrap());
    }

    // inline compressed: items with non-zero indices and a
    // compressible title, packed CBOR is larger because each item
    // carries a separate index field, compressed inline wins
    {
      let properties = Properties {
        gallery: (0..2)
          .map(|i: u8| {
            let mut txid = [0u8; 32];
            for (j, byte) in txid.iter_mut().enumerate() {
              *byte = i.wrapping_mul(17).wrapping_add(j.try_into().unwrap());
            }
            item_with_attributes(
              Txid::from_byte_array(txid),
              (100 + i).into(),
              &format!("title-{i:x}"),
            )
          })
          .collect(),
        ..default()
      };

      let (bytes, encoding) = Inscription::encode_properties(true, &properties).unwrap();
      let bytes = bytes.unwrap();

      assert!(encoding.is_some());

      let inline = properties.to_inline_cbor().unwrap();
      let packed = properties.to_packed_cbor().unwrap();
      let (compressed_inline, _) = Inscription::compress_properties(inline).unwrap();
      let (compressed_packed, _) = Inscription::compress_properties(packed).unwrap();

      assert_eq!(bytes, compressed_inline);
      assert!(compressed_inline.len() < compressed_packed.len());
    }

    // packed compressed: many items sharing a txid, the txid blob
    // contains repeated 32-byte sequences that brotli back-references
    {
      let properties = Properties {
        gallery: (0..20)
          .map(|i| item_with_attributes(txid_a, i, &format!("title-{i:x}")))
          .collect(),
        ..default()
      };

      let (bytes, encoding) = Inscription::encode_properties(true, &properties).unwrap();
      let bytes = bytes.unwrap();

      assert!(encoding.is_some());

      let inline = properties.to_inline_cbor().unwrap();
      let packed = properties.to_packed_cbor().unwrap();
      let (compressed_inline, _) = Inscription::compress_properties(inline).unwrap();
      let (compressed_packed, _) = Inscription::compress_properties(packed).unwrap();

      assert_eq!(bytes, compressed_packed);
      assert!(compressed_packed.len() < compressed_inline.len());
    }
  }

  #[test]
  fn properties_cbor_exceeds_compression_ratio() {
    let cbor = vec![0u8; 1001];

    let mut compressed = Vec::new();

    CompressorWriter::new(&mut compressed, BROTLI_BUFFER_SIZE, 11, 22)
      .write_all(&cbor)
      .unwrap();

    assert!(compressed.len() * MAX_PROPERTIES_COMPRESSION_RATIO < cbor.len());

    assert!(
      Inscription {
        properties: Some(compressed),
        property_encoding: Some(BROTLI.into()),
        ..default()
      }
      .properties_cbor()
      .is_none()
    );
  }
}
