// SeqIO test companion: runs the same battery of SeqIO test cases as
// tests/python_seqio_reference.py and emits results between case markers,
// matching BioPython's output exactly so the two can be diffed.
//
// Run:  moon run cmd/main

///|
/// Escape a string for inclusion in a JSON string literal, matching
/// Python's json.dumps string escaping for the characters that appear in
/// our test data (backslash, double-quote, newline, tab).
fn json_escape(s : String) -> String {
  let buf = StringBuilder(size_hint=s.length() + 2)
  for c in s.iter() {
    match c {
      '\\' => buf.write_string("\\\\")
      '"' => buf.write_string("\\\"")
      '\n' => buf.write_string("\\n")
      '\t' => buf.write_string("\\t")
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Build a JSON string literal (with surrounding double quotes).
fn json_string(s : String) -> String {
  "\"" + json_escape(s) + "\""
}

///|
/// Convert a SeqRecord to a JSON object string, matching BioPython's
/// record_to_dict. Key order: id, name, description, seq, length[, qual].
fn record_to_json(rec : @lib.SeqRecord, include_qual~ : Bool = false) -> String {
  let buf = StringBuilder()
  buf.write_string("{\"id\": ")
  buf.write_string(json_string(rec.id))
  buf.write_string(", \"name\": ")
  buf.write_string(json_string(rec.name))
  buf.write_string(", \"description\": ")
  buf.write_string(json_string(rec.description))
  buf.write_string(", \"seq\": ")
  buf.write_string(json_string(rec.seq.to_string()))
  buf.write_string(", \"length\": ")
  buf.write_string(rec.length().to_string())
  if include_qual {
    buf.write_string(", \"qual\": ")
    let quals : Array[Int32] = match rec.phred_quality() {
      Some(q) => q
      None => Array(capacity=0)
    }
    buf.write_char('[')
    for i, q in quals {
      if i > 0 {
        buf.write_string(", ")
      }
      buf.write_string(q.to_string())
    }
    buf.write_char(']')
  }
  buf.write_char('}')
  buf.to_string()
}

///|
/// Convert an array of SeqRecords to a JSON array string.
fn records_to_json(
  records : Array[@lib.SeqRecord],
  include_qual~ : Bool = false,
) -> String {
  let buf = StringBuilder()
  buf.write_char('[')
  for i, rec in records {
    if i > 0 {
      buf.write_string(", ")
    }
    buf.write_string(record_to_json(rec, include_qual~))
  }
  buf.write_char(']')
  buf.to_string()
}

///|
/// Emit a test case between markers, matching python_seqio_reference.emit.
fn emit(name : String, payload : String) -> Unit {
  println("=== CASE: " + name + " ===")
  println(payload)
  println("=== END ===")
}

///|
/// Run every SeqIO test case, mirroring tests/python_seqio_reference.py.
fn seqio_test() -> Unit {
  // --- Test FASTA content (verbatim from python_seqio_reference.py) ---
  let fasta_multi = ">seq1 Description for seq1\nATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG\n>seq2 Another description\nATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG\n>seq3\nACGTACGTACGTACGT\n"
  let fasta_lowercase = ">lower1 lowercase sequence\natggccattgtaatgggccgctgaaagggtgcccgatag\n>lower2 MixedCase Sequence\natgGCCattGTAatgGGCcgctGAAagggtgccCGATAG\n"
  let fasta_multiline = ">multi1 Multi-line sequence\nATGGCCATTGTA\nATGGGCCGCTGAA\nAGGGTGCCCGATAG\n>multi2 Another multi-line\nACGT\nACGT\nACGT\nACGT\n"
  let fasta_single = ">only One record only\nATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG\n"
  // FASTQ (Sanger/Phred+33). seq1 quality is 39 'I' (Q=40) matching the
  // 39-character sequence.
  let fastq_multi = "@seq1 Description for seq1\nATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG\n+\n" +
    "I".repeat(39) +
    "\n@seq2 Another description\nACGTACGT\n+\nIIIIIIII\n@seq3 No quality worth mentioning\natggcc\n+\n;;;;;;\n"

  // 1. parse_fasta_multiple
  let recs = @lib.seqio_parse(fasta_multi, "fasta") catch {
    _ => Array(capacity=0)
  }
  emit("parse_fasta_multiple", records_to_json(recs))

  // 2. parse_fasta_lowercase
  let recs_l = @lib.seqio_parse(fasta_lowercase, "fasta") catch {
    _ => Array(capacity=0)
  }
  emit("parse_fasta_lowercase", records_to_json(recs_l))

  // 3. parse_fasta_multiline
  let recs_m = @lib.seqio_parse(fasta_multiline, "fasta") catch {
    _ => Array(capacity=0)
  }
  emit("parse_fasta_multiline", records_to_json(recs_m))

  // 4. read_single_record
  let single = @lib.seqio_read(fasta_single, "fasta") catch {
    _ => @lib.SeqRecord::from_seq(@lib.Seq::new(""))
  }
  emit("read_single_record", record_to_json(single))

  // 5. read_multiple_error — SeqIO.read on multi-record raises ValueError.
  let err_payload : String = try {
    ignore(@lib.seqio_read(fasta_multi, "fasta"))
    "{\"error\": null}"
  } catch {
    _ =>
      "{\"error\": \"ValueError\", \"message\": \"More than one record found in handle\"}"
  }
  emit("read_multiple_error", err_payload)

  // 6. to_dict — keyed by id, ordered by record appearance (matching
  // Python dict insertion order).
  let d = @lib.seqio_to_dict(recs)
  let dbuf = StringBuilder()
  dbuf.write_char('{')
  for i, rec in recs {
    if i > 0 {
      dbuf.write_string(", ")
    }
    dbuf.write_string(json_string(rec.id))
    dbuf.write_string(": ")
    let v = match d.get(rec.id) {
      Some(r) => r
      None => rec
    }
    dbuf.write_string(record_to_json(v))
  }
  dbuf.write_char('}')
  emit("to_dict", dbuf.to_string())

  // 7. write_fasta
  let write_recs = [
    @lib.SeqRecord::new(
      @lib.Seq::new("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG"),
      id="written1",
      name="written1",
      description="written1 First written record",
    ),
    @lib.SeqRecord::new(
      @lib.Seq::new("acgtacgtacgt"),
      id="written2",
      name="written2",
      description="written2 lowercase input",
    ),
    @lib.SeqRecord::new(
      @lib.Seq::new("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG"),
      id="written3",
      name="written3",
      description="written3",
    ),
  ]
  let fasta_out = @lib.seqio_write(write_recs, "fasta") catch { _ => "" }
  emit("write_fasta", json_string(fasta_out))

  // 8. parse_fastq
  let fq_recs = @lib.seqio_parse(fastq_multi, "fastq") catch {
    _ => Array(capacity=0)
  }
  emit("parse_fastq", records_to_json(fq_recs, include_qual=true))

  // 9. write_fastq
  let fq1_quals : Array[Int32] = Array(capacity=0)
  for i = 0; i < 39; i = i + 1 {
    fq1_quals.push(40)
  }
  let write_fq_recs = [
    @lib.SeqRecord::new(
      @lib.Seq::new("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG"),
      id="fq1",
      name="fq1",
      description="fq1 written fastq record",
    ).set_phred_quality(fq1_quals),
    @lib.SeqRecord::new(
      @lib.Seq::new("acgt"),
      id="fq2",
      name="fq2",
      description="fq2 lowercase",
    ).set_phred_quality([10, 20, 30, 40]),
  ]
  let fq_out = @lib.seqio_write(write_fq_recs, "fastq") catch { _ => "" }
  emit("write_fastq", json_string(fq_out))

  // 10. parse_genbank
  let genbank_sample = "LOCUS       NM_001              48 bp    DNA     linear   PRI 01-JAN-2020\nDEFINITION  Test gene for comparison.\nACCESSION   NM_001\nVERSION     NM_001.1\nSOURCE      Test organism\n  ORGANISM  Test organism\n            Eukaryota; Test.\nFEATURES             Location/Qualifiers\n     source          1..48\n                     /organism=\"Test organism\"\n     gene            1..48\n                     /gene=\"test_gene\"\n     CDS             1..48\n                     /translation=\"MKRHPGSTH\"\nORIGIN\n        1 atgcatgcat gcatgcatgc atgcatgcat gcatgcatgc atgcatgc\n//\n"
  let gb_recs = @lib.seqio_parse(genbank_sample, "genbank") catch {
    _ => Array(capacity=0)
  }
  let gb_json = match gb_recs.length() {
    0 => "{}"
    _ => record_to_json(gb_recs[0])
  }
  emit("parse_genbank", gb_json)
}