// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// Integration tests for betlangiser — validates manifest parsing,
// code generation, distribution parameters, ternary logic (Kleene algebra),
// confidence intervals, and end-to-end workflows.

use betlangiser::abi::{ConfidenceInterval, Distribution, TernaryBool};
use betlangiser::codegen::codegen::generate_betlang_code;
use betlangiser::manifest::{
    Manifest, ProjectConfig, SimulationConfig, VariableDecl, load_manifest, validate,
};

// ---------------------------------------------------------------------------
// Manifest tests
// ---------------------------------------------------------------------------

/// Test that `init_manifest` creates a valid betlangiser.toml file.
#[test]
fn test_init_creates_manifest() {
    let tmp = tempfile::tempdir().unwrap();
    let path = tmp.path().to_str().unwrap();

    betlangiser::manifest::init_manifest(path).unwrap();

    let manifest_path = tmp.path().join("betlangiser.toml");
    assert!(manifest_path.exists(), "betlangiser.toml should be created");

    // The created manifest should parse and validate successfully.
    let m = load_manifest(manifest_path.to_str().unwrap()).unwrap();
    assert!(validate(&m).is_ok());
    assert_eq!(m.project.name, "my-model");
    assert!(!m.variables.is_empty());
}

/// Test that `init_manifest` refuses to overwrite an existing file.
#[test]
fn test_init_refuses_overwrite() {
    let tmp = tempfile::tempdir().unwrap();
    let path = tmp.path().to_str().unwrap();

    // First init succeeds.
    betlangiser::manifest::init_manifest(path).unwrap();
    // Second init should fail.
    assert!(betlangiser::manifest::init_manifest(path).is_err());
}

// ---------------------------------------------------------------------------
// Code generation tests
// ---------------------------------------------------------------------------

/// Test that `generate_betlang_code` produces valid Betlang source
/// with distribution declarations, propagation rules, and an entry point.
#[test]
fn test_generate_produces_betlang_code() {
    let m = make_risk_model_manifest();
    let code = generate_betlang_code(&m).unwrap();

    // Header and module declaration.
    assert!(
        code.contains("Generated by betlangiser"),
        "Should contain generation header"
    );
    assert!(
        code.contains("module risk_model where"),
        "Should contain module declaration"
    );

    // Distribution declarations for all three variables.
    assert!(
        code.contains("let price : Prob<Float> = Normal(100, 5)"),
        "Should declare price as Normal"
    );
    assert!(
        code.contains("let demand : Prob<Float> = Uniform(50, 150)"),
        "Should declare demand as Uniform"
    );
    assert!(
        code.contains("let will_buy : Prob<Ternary> = Bernoulli(0.7)"),
        "Should declare will_buy as Bernoulli"
    );

    // Propagation rules.
    assert!(
        code.contains("@propagation(price, \"linear-error-propagation\")"),
        "Normal variables use linear error propagation"
    );
    assert!(
        code.contains("@propagation(demand, \"interval-arithmetic\")"),
        "Uniform variables use interval arithmetic"
    );
    assert!(
        code.contains("@propagation(will_buy, \"kleene-ternary\")"),
        "Bernoulli variables use Kleene ternary logic"
    );

    // Ternary helpers (present because of Bernoulli variable).
    assert!(
        code.contains("let ternary_and"),
        "Should include ternary AND"
    );
    assert!(code.contains("let ternary_or"), "Should include ternary OR");
    assert!(
        code.contains("let ternary_not"),
        "Should include ternary NOT"
    );
    assert!(
        code.contains("let will_buy_ternary"),
        "Should include per-variable ternary evaluator"
    );

    // Simulation config.
    assert!(code.contains("@config(samples = 10000)"));
    assert!(code.contains("@config(confidence = 0.95)"));
    assert!(code.contains("@config(seed = 42)"));

    // Entry point.
    assert!(code.contains("let simulate()"));
}

// ---------------------------------------------------------------------------
// Distribution parameter tests
// ---------------------------------------------------------------------------

/// Test that Normal distribution parameters are correctly validated.
#[test]
fn test_normal_distribution_params() {
    // Valid construction.
    let d = Distribution::new_normal(100.0, 5.0).unwrap();
    assert_eq!(d.kind(), "normal");
    match d {
        Distribution::Normal { mean, std_dev } => {
            assert_eq!(mean, 100.0);
            assert_eq!(std_dev, 5.0);
        }
        _ => panic!("Expected Normal variant"),
    }

    // Invalid: zero std_dev.
    assert!(
        Distribution::new_normal(0.0, 0.0).is_err(),
        "Zero std_dev should be rejected"
    );

    // Invalid: negative std_dev.
    assert!(
        Distribution::new_normal(0.0, -1.0).is_err(),
        "Negative std_dev should be rejected"
    );
}

/// Test that all distribution types can be constructed with valid parameters.
#[test]
fn test_all_distributions_valid() {
    let normal = Distribution::new_normal(0.0, 1.0);
    assert!(normal.is_ok(), "Normal(0, 1) should be valid");

    let uniform = Distribution::new_uniform(0.0, 10.0);
    assert!(uniform.is_ok(), "Uniform(0, 10) should be valid");

    let beta = Distribution::new_beta(2.0, 5.0);
    assert!(beta.is_ok(), "Beta(2, 5) should be valid");

    let bernoulli = Distribution::new_bernoulli(0.7);
    assert!(bernoulli.is_ok(), "Bernoulli(0.7) should be valid");

    let custom = Distribution::new_custom("mixture(0.5, normal(0,1), normal(5,2))".to_string());
    assert!(custom.is_ok(), "Custom with expression should be valid");

    // Verify kind() for each.
    assert_eq!(normal.unwrap().kind(), "normal");
    assert_eq!(uniform.unwrap().kind(), "uniform");
    assert_eq!(beta.unwrap().kind(), "beta");
    assert_eq!(bernoulli.unwrap().kind(), "bernoulli");
    assert_eq!(custom.unwrap().kind(), "custom");
}

// ---------------------------------------------------------------------------
// Ternary logic tests (Kleene algebra)
// ---------------------------------------------------------------------------

/// Test Kleene strong three-valued AND — complete truth table.
#[test]
fn test_ternary_logic_and() {
    use TernaryBool::*;

    // True AND x
    assert_eq!(True.and(True), True);
    assert_eq!(True.and(False), False);
    assert_eq!(True.and(Unknown), Unknown);

    // False AND x (absorbing element)
    assert_eq!(False.and(True), False);
    assert_eq!(False.and(False), False);
    assert_eq!(False.and(Unknown), False);

    // Unknown AND x
    assert_eq!(Unknown.and(True), Unknown);
    assert_eq!(Unknown.and(False), False);
    assert_eq!(Unknown.and(Unknown), Unknown);
}

/// Test Kleene strong three-valued OR — complete truth table.
#[test]
fn test_ternary_logic_or() {
    use TernaryBool::*;

    // True OR x (absorbing element)
    assert_eq!(True.or(True), True);
    assert_eq!(True.or(False), True);
    assert_eq!(True.or(Unknown), True);

    // False OR x
    assert_eq!(False.or(True), True);
    assert_eq!(False.or(False), False);
    assert_eq!(False.or(Unknown), Unknown);

    // Unknown OR x
    assert_eq!(Unknown.or(True), True);
    assert_eq!(Unknown.or(False), Unknown);
    assert_eq!(Unknown.or(Unknown), Unknown);
}

/// Test Kleene strong three-valued NOT.
#[test]
fn test_ternary_logic_not() {
    assert_eq!(TernaryBool::True.not(), TernaryBool::False);
    assert_eq!(TernaryBool::False.not(), TernaryBool::True);
    assert_eq!(TernaryBool::Unknown.not(), TernaryBool::Unknown);
}

/// Test De Morgan's laws hold for Kleene ternary logic.
#[test]
fn test_ternary_de_morgan() {
    let values = [TernaryBool::True, TernaryBool::False, TernaryBool::Unknown];
    for &a in &values {
        for &b in &values {
            // NOT (A AND B) == (NOT A) OR (NOT B)
            assert_eq!(a.and(b).not(), a.not().or(b.not()));
            // NOT (A OR B) == (NOT A) AND (NOT B)
            assert_eq!(a.or(b).not(), a.not().and(b.not()));
        }
    }
}

// ---------------------------------------------------------------------------
// Confidence interval tests
// ---------------------------------------------------------------------------

/// Test ConfidenceInterval construction and methods.
#[test]
fn test_confidence_interval() {
    let ci = ConfidenceInterval::new(90.0, 110.0, 0.95).unwrap();

    assert_eq!(ci.lower, 90.0);
    assert_eq!(ci.upper, 110.0);
    assert_eq!(ci.confidence, 0.95);
    assert_eq!(ci.width(), 20.0);
    assert_eq!(ci.midpoint(), 100.0);

    // Containment checks.
    assert!(ci.contains(100.0));
    assert!(ci.contains(90.0));
    assert!(ci.contains(110.0));
    assert!(!ci.contains(89.9));
    assert!(!ci.contains(110.1));
}

/// Test that invalid confidence intervals are rejected.
#[test]
fn test_confidence_interval_invalid() {
    // Inverted bounds.
    assert!(ConfidenceInterval::new(110.0, 90.0, 0.95).is_err());
    // Confidence out of range.
    assert!(ConfidenceInterval::new(0.0, 1.0, 0.0).is_err());
    assert!(ConfidenceInterval::new(0.0, 1.0, 1.0).is_err());
}

// ---------------------------------------------------------------------------
// End-to-end tests
// ---------------------------------------------------------------------------

/// Test the full pipeline: write manifest to disk, load, validate, generate.
#[test]
fn test_end_to_end_generate() {
    let tmp = tempfile::tempdir().unwrap();
    let manifest_path = tmp.path().join("betlangiser.toml");
    let output_dir = tmp.path().join("generated");

    // Write a manifest to disk.
    std::fs::write(
        &manifest_path,
        r#"
[project]
name = "e2e-test"

[[variables]]
name = "temperature"
distribution = "normal"
mean = 20.0
std-dev = 3.0

[[variables]]
name = "sensor_ok"
distribution = "bernoulli"
probability = 0.95

[simulation]
samples = 5000
confidence = 0.99
output-format = "json"
"#,
    )
    .unwrap();

    // Load and validate.
    let m = load_manifest(manifest_path.to_str().unwrap()).unwrap();
    assert!(validate(&m).is_ok());
    assert_eq!(m.variables.len(), 2);

    // Generate code.
    betlangiser::generate(
        manifest_path.to_str().unwrap(),
        output_dir.to_str().unwrap(),
    )
    .unwrap();

    // Check that the output file exists.
    let output_file = output_dir.join("e2e_test.bet");
    assert!(
        output_file.exists(),
        "Generated Betlang file should exist at {}",
        output_file.display()
    );

    // Read and verify generated code.
    let code = std::fs::read_to_string(&output_file).unwrap();
    assert!(code.contains("let temperature : Prob<Float>"));
    assert!(code.contains("let sensor_ok : Prob<Ternary>"));
    assert!(code.contains("@config(samples = 5000)"));
    assert!(code.contains("@config(confidence = 0.99)"));
    assert!(code.contains("@config(output = \"json\")"));
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Create the risk-model manifest used in multiple tests — matches the
/// example at examples/risk-model/betlangiser.toml.
fn make_risk_model_manifest() -> Manifest {
    Manifest {
        project: ProjectConfig {
            name: "risk-model".to_string(),
            description: Some("Risk model with probabilistic pricing and demand".to_string()),
        },
        variables: vec![
            VariableDecl {
                name: "price".to_string(),
                distribution: "normal".to_string(),
                mean: Some(100.0),
                std_dev: Some(5.0),
                min: None,
                max: None,
                alpha: None,
                beta_param: None,
                probability: None,
                expression: None,
            },
            VariableDecl {
                name: "demand".to_string(),
                distribution: "uniform".to_string(),
                mean: None,
                std_dev: None,
                min: Some(50.0),
                max: Some(150.0),
                alpha: None,
                beta_param: None,
                probability: None,
                expression: None,
            },
            VariableDecl {
                name: "will_buy".to_string(),
                distribution: "bernoulli".to_string(),
                mean: None,
                std_dev: None,
                min: None,
                max: None,
                alpha: None,
                beta_param: None,
                probability: Some(0.7),
                expression: None,
            },
        ],
        simulation: SimulationConfig {
            samples: 10_000,
            confidence: 0.95,
            seed: Some(42),
            output_format: "text".to_string(),
        },
    }
}
