//! Shape-only JSON comparison for Rust vs reference Better Auth HTTP responses.
#![allow(dead_code)]

use serde_json::Value;
use std::collections::BTreeSet;

/// Structural shape (field names + value types) for comparing two servers.
pub fn json_shape(value: &Value) -> Value {
    match value {
        Value::Null => Value::String("null".into()),
        Value::Bool(_) => Value::String("boolean".into()),
        Value::Number(_) => Value::String("number".into()),
        Value::String(_) => Value::String("string".into()),
        Value::Array(arr) => {
            if let Some(first) = arr.first() {
                Value::Array(vec![json_shape(first)])
            } else {
                Value::Array(vec![])
            }
        }
        Value::Object(map) => {
            let shaped: serde_json::Map<String, Value> = map
                .iter()
                .map(|(k, v)| (k.clone(), json_shape(v)))
                .collect();
            Value::Object(shaped)
        }
    }
}

/// Compare two shapes and collect human-readable differences.
pub fn compare_shapes(rust_shape: &Value, ref_shape: &Value, path: &str) -> Vec<String> {
    let mut diffs = Vec::new();

    match (rust_shape, ref_shape) {
        (Value::Object(r), Value::Object(n)) => {
            let r_keys: BTreeSet<_> = r.keys().collect();
            let n_keys: BTreeSet<_> = n.keys().collect();

            for key in r_keys.difference(&n_keys) {
                diffs.push(format!(
                    "{}.{}: present in Rust, missing in reference",
                    path, key
                ));
            }
            for key in n_keys.difference(&r_keys) {
                diffs.push(format!(
                    "{}.{}: missing in Rust, present in reference",
                    path, key
                ));
            }
            for key in r_keys.intersection(&n_keys) {
                let child_path = if path.is_empty() {
                    key.to_string()
                } else {
                    format!("{}.{}", path, key)
                };
                diffs.extend(compare_shapes(&r[*key], &n[*key], &child_path));
            }
        }
        (Value::Array(r), Value::Array(n)) => {
            if let (Some(r0), Some(n0)) = (r.first(), n.first()) {
                diffs.extend(compare_shapes(r0, n0, &format!("{}[]", path)));
            }
        }
        (Value::String(r), Value::String(n)) if r != n => {
            diffs.push(format!("{}: Rust={}, reference={}", path, r, n));
        }
        _ => {}
    }

    diffs
}

const KNOWN_EXTRA_RUST_USER_FIELDS: &[&str] = &[
    "banned",
    "banReason",
    "banExpires",
    "role",
    "twoFactorEnabled",
    "lastSignInAt",
    "username",
    "displayUsername",
];

const KNOWN_EXTRA_RUST_SESSION_FIELDS: &[&str] = &["activeOrganizationId", "impersonatedBy"];

/// Strip expected Rust-vs-TS harness differences from a diff list.
pub fn filter_known_diffs(diffs: &[String]) -> Vec<String> {
    diffs
        .iter()
        .filter(|d| {
            for field in KNOWN_EXTRA_RUST_USER_FIELDS {
                if d.contains(&format!(".{}: present in Rust", field)) {
                    return false;
                }
            }
            for field in KNOWN_EXTRA_RUST_SESSION_FIELDS {
                if d.contains(&format!(".{}: present in Rust", field)) {
                    return false;
                }
            }
            if d.contains(".url: present in Rust") {
                return false;
            }
            if d.contains(".code: missing in Rust") {
                return false;
            }
            if (d.contains("ipAddress:") || d.contains("userAgent:"))
                && d.contains("Rust=null")
                && d.contains("reference=string")
            {
                return false;
            }
            true
        })
        .cloned()
        .collect()
}
