use serde::Deserialize;
use shelly::{
    validate_client_message_invariants, validate_server_message_invariants,
    validate_server_message_sequence, ClientMessage, ServerMessage,
};
use std::{fs, path::PathBuf};

#[derive(Debug, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum RegressionCase {
    ClientMessage {
        message: ClientMessage,
        expected_error: Option<String>,
    },
    ServerMessage {
        message: ServerMessage,
        expected_error: Option<String>,
    },
    ServerSequence {
        messages: Vec<ServerMessage>,
        expected_error: Option<String>,
    },
}

fn read_cases() -> Vec<(String, RegressionCase)> {
    let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("protocol")
        .join("regression");
    let mut entries = fs::read_dir(&dir)
        .unwrap_or_else(|err| panic!("failed to list regression corpus {}: {err}", dir.display()))
        .map(|entry| entry.expect("regression corpus entry").path())
        .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("json"))
        .collect::<Vec<_>>();
    entries.sort();
    entries
        .into_iter()
        .map(|path| {
            let name = path
                .file_name()
                .expect("filename")
                .to_string_lossy()
                .to_string();
            let source = fs::read_to_string(&path)
                .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()));
            let case: RegressionCase = serde_json::from_str(&source)
                .unwrap_or_else(|err| panic!("failed to parse {}: {err}", path.display()));
            (name, case)
        })
        .collect()
}

#[test]
fn regression_corpus_cases_match_expected_outcomes() {
    let cases = read_cases();
    assert!(!cases.is_empty(), "regression corpus should not be empty");

    for (name, case) in cases {
        match case {
            RegressionCase::ClientMessage {
                message,
                expected_error,
            } => {
                let result = validate_client_message_invariants(&message);
                assert_case_result(&name, expected_error, result.err().map(|err| err.code));
            }
            RegressionCase::ServerMessage {
                message,
                expected_error,
            } => {
                let result = validate_server_message_invariants(&message);
                assert_case_result(&name, expected_error, result.err().map(|err| err.code));
            }
            RegressionCase::ServerSequence {
                messages,
                expected_error,
            } => {
                let result = validate_server_message_sequence(&messages);
                assert_case_result(&name, expected_error, result.err().map(|err| err.code));
            }
        }
    }
}

fn assert_case_result(name: &str, expected_error: Option<String>, actual_error: Option<String>) {
    match (expected_error.as_deref(), actual_error.as_deref()) {
        (None, None) => {}
        (Some(expected), Some(actual)) => assert_eq!(
            actual, expected,
            "regression case '{name}' emitted unexpected error code"
        ),
        (None, Some(actual)) => {
            panic!("regression case '{name}' should pass, got error '{actual}'")
        }
        (Some(expected), None) => {
            panic!("regression case '{name}' should fail with '{expected}'")
        }
    }
}
