// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// A classification attached to one generated test case.
///
/// Construct observations with `label`, `classify`, and `collect`.
enum Observation {
  Label(String)
  Class(label~ : String, hit~ : Bool)
} derive(@debug.Debug)

///|
/// Observations attached to one generated test case.
type Observations = Array[Observation]

///|
/// Aggregated observations from successful test cases.
///
/// `labels` counts each case's sorted set of labels as one joint bucket.
/// `classes` counts each named class independently.
priv struct ObservationSummary {
  labels : Map[@list.List[String], UInt]
  classes : Map[String, UInt]
}

///|
/// Snapshot-friendly rendering: empty categories are omitted, so a
/// classes-only summary renders as `{ classes: {...} }` without a noisy
/// `labels: {}` field (and vice versa).
impl @debug.Debug for ObservationSummary with fn to_repr(self) {
  @debug.Repr::record(
    Map(
      [
        ..if !self.labels.is_empty() {
          [("labels", @debug.Repr(self.labels))]
        },
        ..if !self.classes.is_empty() {
          [("classes", @debug.Repr(self.classes))]
        },
      ],
    ),
  )
}

///|
pub impl Show for Observation with fn output(self, logger) {
  match self {
    Label(value) => logger.write_string(value)
    Class(label~, ..) => logger.write_string(label)
  }
}

///|
#deprecated("Use `@debug.Debug::to_repr` instead")
#doc(hidden)
pub extend Observation with @debug.Debug::{to_repr}

///|
/// Attaches `value` as a label to a generated test case.
pub fn label(value : String) -> Observation {
  Label(value)
}

///|
/// Classifies a generated test case under `label` when `condition` is true.
pub fn classify(condition : Bool, label : String) -> Observation {
  Class(label~, hit=condition)
}

///|
/// Attaches the debug representation of `value` as a label.
pub fn[T : @debug.Debug] collect(value : T) -> Observation {
  label(@debug.to_string(value))
}

///|
fn ObservationSummary::empty() -> ObservationSummary {
  { labels: {}, classes: {} }
}

///|
fn ObservationSummary::is_empty(self : ObservationSummary) -> Bool {
  self.labels.is_empty() && self.classes.is_empty()
}

///|
fn ObservationSummary::add(
  self : ObservationSummary,
  observations : Observations,
) -> Unit {
  let labels = []
  let classes : Map[String, Bool] = Map([])
  for observation in observations {
    match observation {
      Label(value) => labels.push(value)
      Class(label~, hit~) =>
        classes[label] = classes.get_or_default(label, false) || hit
    }
  }
  labels.sort()
  labels.dedup()
  if !labels.is_empty() {
    let key = @list.List(labels)
    self.labels[key] = self.labels.get_or_default(key, 0U) + 1U
  }
  for label, hit in classes {
    let increment = if hit { 1U } else { 0U }
    self.classes[label] = self.classes.get_or_default(label, 0U) + increment
  }
}

///|
priv struct ObservationRow {
  count : UInt
  name : String
} derive(Eq)

///|
impl Compare for ObservationRow with fn compare(
  left : ObservationRow,
  right : ObservationRow,
) {
  match Compare::compare(right.count, left.count) {
    0 => Compare::compare(left.name, right.name)
    order => order
  }
}

///|
fn percentage(count : UInt, total : UInt) -> UInt {
  guard total > 0U else { return 0U }
  let total = total.to_uint64()
  ((count.to_uint64() * 100UL + total / 2UL) / total).to_uint()
}

///|
fn write_observation_rows(
  builder : StringBuilder,
  heading : String,
  rows : Array[ObservationRow],
  total : UInt,
  count_width : Int,
) -> Unit {
  guard !rows.is_empty() else { return }
  rows.sort_by(Compare::compare)
  builder <+ "\n  \{heading}:"
  for row in rows {
    let count = "\{row.count}".pad_start(count_width, ' ')
    let percent = "\{percentage(row.count, total)}".pad_start(3, ' ')
    builder <+ "\n    \{count}  \{percent}%  \{row.name}"
  }
}

///|
fn ObservationSummary::report(
  self : ObservationSummary,
  total : UInt,
  prefix? : String = "",
) -> String {
  guard !self.is_empty() else { return "" }
  let label_rows = [
    for labels, count in self.labels => {
      { count, name: labels.iter().join(", ") }
    }
  ]
  let class_rows = [ for name, count in self.classes => { count, name } ]
  let builder = StringBuilder()
  builder <+ "\{prefix}observations:"
  let count_width = "\{total}".length()
  write_observation_rows(builder, "labels", label_rows, total, count_width)
  write_observation_rows(builder, "classes", class_rows, total, count_width)
  builder.to_string()
}

///|
pub extend Observation with Show::{to_string}

// --- deprecated: hidden from the generated interface ---

///|
#deprecated("Use `Show::output` via the trait or `to_string` instead", skip_current_package=true)
#doc(hidden)
pub extend Observation with Show::{output}