//! `lifeloop conformance run` — drive the `tests/conformance/`
//! fixture corpus from the CLI.
//!
//! Walks every `*.json` file under `--root` (default
//! `tests/conformance`), parses the self-describing fixture envelope,
//! and runs the same `validate()` per-kind dispatch the in-process
//! `tests/conformance.rs` test does. Emits one JSONL line per fixture
//! to stdout:
//!
//! ```json
//! { "path": "...", "kind": "...", "expect": "valid", "outcome": "valid_ok" }
//! ```
//!
//! Exits non-zero on the first fixture whose actual outcome does not
//! match its declared `expect` (validation-class). The CLI does not
//! re-implement the kinds — it deserializes via the same public types
//! the test uses (`CallbackRequest::validate`, `CallbackResponse::validate`,
//! etc.).

use std::fs;
use std::path::{Path, PathBuf};

use lifeloop::router::{
    LifeloopFailureMapper, TransportError, classes_for_negotiation_outcome,
    failure_class_for_transport, retry_class_for,
};
use lifeloop::telemetry::PressureObservation;
use lifeloop::{
    AdapterManifest, CallbackRequest, CallbackResponse, FailureClass, FrameContext,
    LifecycleReceipt, NegotiationOutcome, PayloadEnvelope, RetryClass,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::{CliError, print_json};

#[derive(Debug, Deserialize)]
struct Fixture {
    kind: String,
    expect: Expect,
    #[serde(default)]
    expect_error: Option<String>,
    #[serde(default)]
    description: Option<String>,
    data: Value,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
enum Expect {
    Valid,
    Invalid,
}

#[derive(Serialize)]
struct FixtureRow<'a> {
    path: &'a str,
    kind: &'a str,
    expect: Expect,
    outcome: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    description: Option<&'a str>,
}

pub fn run<I: Iterator<Item = String>>(mut args: I) -> Result<(), CliError> {
    let action = args
        .next()
        .ok_or_else(|| CliError::Usage("conformance requires a subcommand: run".to_string()))?;
    match action.as_str() {
        "run" => run_run(args),
        other => Err(CliError::Usage(format!(
            "conformance: unknown subcommand `{other}` (expected: run)"
        ))),
    }
}

fn run_run<I: Iterator<Item = String>>(mut args: I) -> Result<(), CliError> {
    let mut root: Option<PathBuf> = None;
    let mut summary_only = false;
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--root" => {
                let v = args
                    .next()
                    .ok_or_else(|| CliError::Usage("--root requires a path".into()))?;
                root = Some(PathBuf::from(v));
            }
            "--summary" => {
                summary_only = true;
            }
            other => {
                return Err(CliError::Usage(format!(
                    "conformance run: unknown flag `{other}`"
                )));
            }
        }
    }
    let root = root.unwrap_or_else(|| {
        // Default to repo-relative tests/conformance — caller can
        // override with --root if invoked from elsewhere.
        PathBuf::from("tests/conformance")
    });
    if !root.is_dir() {
        return Err(CliError::Input(format!(
            "conformance run: root `{}` is not a directory",
            root.display()
        )));
    }

    let files = collect_fixtures(&root);
    if files.is_empty() {
        return Err(CliError::Validation(format!(
            "conformance run: no fixtures found under `{}`",
            root.display()
        )));
    }

    let mut total = 0usize;
    let mut passed = 0usize;
    for path in &files {
        total += 1;
        let bytes = fs::read(path).map_err(|e| {
            CliError::Input(format!("failed to read fixture `{}`: {e}", path.display()))
        })?;
        let fixture: Fixture = serde_json::from_slice(&bytes).map_err(|e| {
            CliError::Input(format!(
                "failed to parse fixture envelope `{}`: {e}",
                path.display()
            ))
        })?;
        let result = run_one(&fixture);
        let outcome = classify(&fixture, &result);
        if !summary_only {
            let row = FixtureRow {
                path: path.to_str().unwrap_or("<non-utf8 path>"),
                kind: &fixture.kind,
                expect: fixture.expect,
                outcome: outcome.label,
                error: result.err(),
                description: fixture.description.as_deref(),
            };
            // JSONL: one compact line per fixture.
            let line = serde_json::to_string(&row)
                .map_err(|e| CliError::Input(format!("failed to serialize fixture row: {e}")))?;
            println!("{line}");
        }
        if outcome.passed {
            passed += 1;
        } else {
            return Err(CliError::Validation(format!(
                "conformance run: fixture `{}` failed (kind={}, expect={:?}, outcome={})",
                path.display(),
                fixture.kind,
                fixture.expect,
                outcome.label
            )));
        }
    }

    if summary_only {
        let summary = serde_json::json!({
            "total": total,
            "passed": passed,
            "root": root.display().to_string(),
        });
        print_json(&summary)?;
    }
    Ok(())
}

struct OutcomeClass {
    label: &'static str,
    passed: bool,
}

fn classify(fixture: &Fixture, result: &Result<(), String>) -> OutcomeClass {
    match (result, fixture.expect) {
        (Ok(()), Expect::Valid) => OutcomeClass {
            label: "valid_ok",
            passed: true,
        },
        (Err(_), Expect::Invalid) => {
            // If expect_error is set, require the substring match.
            let needle_ok = fixture
                .expect_error
                .as_deref()
                .map(|n| {
                    result
                        .as_ref()
                        .err()
                        .map(|m| m.contains(n))
                        .unwrap_or(false)
                })
                .unwrap_or(true);
            OutcomeClass {
                label: if needle_ok {
                    "invalid_ok"
                } else {
                    "invalid_wrong_message"
                },
                passed: needle_ok,
            }
        }
        (Ok(()), Expect::Invalid) => OutcomeClass {
            label: "expected_invalid_but_valid",
            passed: false,
        },
        (Err(_), Expect::Valid) => OutcomeClass {
            label: "expected_valid_but_invalid",
            passed: false,
        },
    }
}

fn run_one(fixture: &Fixture) -> Result<(), String> {
    match fixture.kind.as_str() {
        "callback_request" => parse_and_validate::<CallbackRequest>(&fixture.data, |x| {
            x.validate().map_err(|e| e.to_string())
        }),
        "callback_response" => parse_and_validate::<CallbackResponse>(&fixture.data, |x| {
            x.validate().map_err(|e| e.to_string())
        }),
        "lifecycle_receipt" => parse_and_validate::<LifecycleReceipt>(&fixture.data, |x| {
            x.validate().map_err(|e| e.to_string())
        }),
        "payload_envelope" => parse_and_validate::<PayloadEnvelope>(&fixture.data, |x| {
            x.validate().map_err(|e| e.to_string())
        }),
        "frame_context" => parse_and_validate::<FrameContext>(&fixture.data, |x| {
            x.validate().map_err(|e| e.to_string())
        }),
        "adapter_manifest" => parse_and_validate::<AdapterManifest>(&fixture.data, |x| {
            x.validate().map_err(|e| e.to_string())
        }),
        "pressure_observation" => parse_only::<PressureObservation>(&fixture.data),
        "client_class" => run_client_class(&fixture.data),
        "capability_negotiation" => run_capability_negotiation(&fixture.data),
        "failure_mapping" => run_failure_mapping(&fixture.data),
        other => Err(format!("unknown fixture kind `{other}`")),
    }
}

fn parse_and_validate<T: serde::de::DeserializeOwned>(
    data: &Value,
    validate: impl FnOnce(&T) -> Result<(), String>,
) -> Result<(), String> {
    let typed: T = serde_json::from_value(data.clone()).map_err(|e| format!("deserialize: {e}"))?;
    validate(&typed)
}

fn parse_only<T: serde::de::DeserializeOwned + serde::Serialize>(
    data: &Value,
) -> Result<(), String> {
    let typed: T = serde_json::from_value(data.clone()).map_err(|e| format!("deserialize: {e}"))?;
    let _back = serde_json::to_value(&typed).map_err(|e| format!("reserialize: {e}"))?;
    Ok(())
}

#[derive(Debug, Deserialize)]
struct ClientClassFixture {
    client_id: String,
    description: String,
    payload_kinds: Vec<String>,
    example_payload: Value,
}

fn run_client_class(data: &Value) -> Result<(), String> {
    let cls: ClientClassFixture =
        serde_json::from_value(data.clone()).map_err(|e| format!("deserialize: {e}"))?;
    if cls.client_id.is_empty() {
        return Err("client_id must not be empty".into());
    }
    if cls.description.is_empty() {
        return Err("description must not be empty".into());
    }
    if cls.payload_kinds.is_empty() {
        return Err("payload_kinds must list at least one kind".into());
    }
    let payload: PayloadEnvelope = serde_json::from_value(cls.example_payload.clone())
        .map_err(|e| format!("example_payload deserialize: {e}"))?;
    payload.validate().map_err(|e| e.to_string())?;
    if payload.client_id != cls.client_id {
        return Err(format!(
            "example_payload.client_id `{}` must match fixture client_id `{}`",
            payload.client_id, cls.client_id
        ));
    }
    if !cls.payload_kinds.contains(&payload.payload_kind) {
        return Err(format!(
            "example_payload.payload_kind `{}` not in declared payload_kinds {:?}",
            payload.payload_kind, cls.payload_kinds
        ));
    }
    Ok(())
}

#[derive(Debug, Deserialize)]
struct CapabilityNegotiationFixture {
    outcome: NegotiationOutcome,
    #[serde(default)]
    explicit_failure_class: Option<FailureClass>,
    expected: Option<FailureRetryPair>,
}

#[derive(Debug, Deserialize)]
struct FailureRetryPair {
    failure_class: FailureClass,
    retry_class: RetryClass,
}

fn run_capability_negotiation(data: &Value) -> Result<(), String> {
    let fx: CapabilityNegotiationFixture =
        serde_json::from_value(data.clone()).map_err(|e| format!("deserialize: {e}"))?;
    let actual = classes_for_negotiation_outcome(fx.outcome, fx.explicit_failure_class);
    match (&actual, &fx.expected) {
        (None, None) => Ok(()),
        (Some((af, ar)), Some(exp)) => {
            if *af == exp.failure_class && *ar == exp.retry_class {
                Ok(())
            } else {
                Err(format!(
                    "negotiation projection mismatch: got ({af:?}, {ar:?}), expected ({:?}, {:?})",
                    exp.failure_class, exp.retry_class
                ))
            }
        }
        (None, Some(exp)) => Err(format!(
            "expected ({:?}, {:?}) but classes_for_negotiation_outcome returned None",
            exp.failure_class, exp.retry_class
        )),
        (Some(pair), None) => Err(format!(
            "expected None but classes_for_negotiation_outcome returned {pair:?}"
        )),
    }
}

#[derive(Debug, Deserialize)]
struct FailureMappingFixture {
    failure_class: FailureClass,
    expected_retry_class: RetryClass,
    #[serde(default)]
    transport_example: Option<TransportExample>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "detail")]
enum TransportExample {
    Io(String),
    Timeout,
    Internal(String),
}

fn run_failure_mapping(data: &Value) -> Result<(), String> {
    let fx: FailureMappingFixture =
        serde_json::from_value(data.clone()).map_err(|e| format!("deserialize: {e}"))?;
    let actual_retry = retry_class_for(fx.failure_class);
    if actual_retry != fx.expected_retry_class {
        return Err(format!(
            "default retry for {:?}: got {actual_retry:?}, expected {:?}",
            fx.failure_class, fx.expected_retry_class
        ));
    }
    if fx.failure_class.default_retry() != fx.expected_retry_class {
        return Err(format!(
            "FailureClass::{:?}::default_retry diverges from expected_retry_class",
            fx.failure_class
        ));
    }
    if let Some(example) = fx.transport_example {
        let te = match example {
            TransportExample::Io(d) => TransportError::Io(d),
            TransportExample::Timeout => TransportError::Timeout,
            TransportExample::Internal(d) => TransportError::Internal(d),
        };
        let mapper = LifeloopFailureMapper::new();
        let (fc, rc) = mapper.map_transport_error(&te);
        let direct_fc = failure_class_for_transport(&te);
        if direct_fc != fc {
            return Err(format!("free-fn vs mapper drift: {direct_fc:?} vs {fc:?}"));
        }
        if fc != fx.failure_class {
            return Err(format!(
                "transport_example maps to {fc:?} but fixture declares {:?}",
                fx.failure_class
            ));
        }
        if rc != fx.expected_retry_class {
            return Err(format!(
                "transport_example retry {rc:?} != expected {:?}",
                fx.expected_retry_class
            ));
        }
    }
    Ok(())
}

fn collect_fixtures(root: &Path) -> Vec<PathBuf> {
    let mut out = Vec::new();
    let mut stack = vec![root.to_path_buf()];
    while let Some(dir) = stack.pop() {
        let entries = match fs::read_dir(&dir) {
            Ok(entries) => entries,
            Err(_) => continue,
        };
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                stack.push(path);
            } else if path.extension().and_then(|s| s.to_str()) == Some("json") {
                out.push(path);
            }
        }
    }
    out.sort();
    out
}
