// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#![allow(clippy::unicode_not_nfc)]

use super::{CircuitEntryPoint, Debugger, Interpreter};
use crate::{
    interpret::{CircuitGenerationMethod, Error},
    target::Profile,
};
use expect_test::expect;
use miette::Diagnostic;
use qsc_circuit::{Circuit, TracerConfig};
use qsc_data_structures::{language_features::LanguageFeatures, source::SourceMap};
use qsc_eval::output::GenericReceiver;
use qsc_eval::val::Value;
use qsc_passes::PackageType;

fn interpreter(code: &str, package_type: PackageType, profile: Profile) -> Interpreter {
    let sources = SourceMap::new([("test.qs".into(), code.into())], None);
    let (std_id, store) = crate::compile::package_store_with_stdlib(profile.into());
    Interpreter::new(
        sources,
        package_type,
        profile.into(),
        LanguageFeatures::default(),
        store,
        &[(std_id, None)],
    )
    .expect("interpreter creation should succeed")
}

fn interpreter_with_circuit_trace(code: &str, profile: Profile) -> Interpreter {
    let sources = SourceMap::new([("test.qs".into(), code.into())], None);
    let (std_id, store) = crate::compile::package_store_with_stdlib(profile.into());
    Interpreter::with_circuit_trace(
        sources,
        PackageType::Exe,
        profile.into(),
        LanguageFeatures::default(),
        store,
        &[(std_id, None)],
        default_test_tracer_config(),
    )
    .expect("interpreter creation should succeed")
}

fn circuit_without_groups(code: &str, entry: CircuitEntryPoint) -> String {
    let eval_circ = circuit_with_options_success(
        code,
        Profile::Unrestricted,
        entry.clone(),
        CircuitGenerationMethod::ClassicalEval,
        TracerConfig {
            group_by_scope: false,
            ..default_test_tracer_config()
        },
    );

    let eval_circ_without_source_locations = circuit_with_options_success(
        code,
        Profile::Unrestricted,
        entry.clone(),
        CircuitGenerationMethod::ClassicalEval,
        TracerConfig {
            group_by_scope: false,
            source_locations: false,
            ..default_test_tracer_config()
        },
    );

    let static_circ_without_source_locations = circuit_with_options_success(
        code,
        Profile::AdaptiveRIF,
        entry,
        CircuitGenerationMethod::Static,
        TracerConfig {
            group_by_scope: false,
            source_locations: false,
            ..default_test_tracer_config()
        },
    );

    // Source locations for qubit allocation are not currently supported in static.
    // For now, we'll just ignore this difference between the classicalEval and static methods.
    assert_eq!(
        eval_circ_without_source_locations.to_string(),
        static_circ_without_source_locations.to_string()
    );

    eval_circ.to_string()
}

fn circuit_with_groups(code: &str, entry: CircuitEntryPoint) -> String {
    let eval_circ = circuit_with_options_success(
        code,
        Profile::Unrestricted,
        entry.clone(),
        CircuitGenerationMethod::ClassicalEval,
        default_test_tracer_config(),
    );

    let eval_circ_without_source_locations = circuit_with_options_success(
        code,
        Profile::Unrestricted,
        entry.clone(),
        CircuitGenerationMethod::ClassicalEval,
        TracerConfig {
            source_locations: false,
            ..default_test_tracer_config()
        },
    );
    let static_circ_without_source_locations = circuit_with_options_success(
        code,
        Profile::AdaptiveRIF,
        entry,
        CircuitGenerationMethod::Static,
        TracerConfig {
            source_locations: false,
            ..default_test_tracer_config()
        },
    );

    // Source locations for qubit allocation are not currently supported in static.
    // For now, we'll just ignore this difference between the classicalEval and static methods.
    assert_eq!(
        eval_circ_without_source_locations
            .display_with_groups()
            .to_string(),
        static_circ_without_source_locations
            .display_with_groups()
            .to_string()
    );

    eval_circ.display_with_groups().to_string()
}

/// Generates a grouped circuit with source locations disabled, asserts that
/// classical evaluation and static generation produce the same grouped display,
/// and returns the static rendering for snapshot comparison.
fn circuit_with_groups_without_source_locations(code: &str, entry: CircuitEntryPoint) -> String {
    let eval_circ = circuit_with_options_success(
        code,
        Profile::Unrestricted,
        entry.clone(),
        CircuitGenerationMethod::ClassicalEval,
        TracerConfig {
            source_locations: false,
            ..default_test_tracer_config()
        },
    );

    let static_circ = circuit_with_options_success(
        code,
        Profile::AdaptiveRIF,
        entry,
        CircuitGenerationMethod::Static,
        TracerConfig {
            source_locations: false,
            ..default_test_tracer_config()
        },
    );

    assert_eq!(
        eval_circ.display_with_groups().to_string(),
        static_circ.display_with_groups().to_string()
    );

    static_circ.display_with_groups().to_string()
}

fn circuit_static(code: &str) -> Circuit {
    circuit_with_options_success(
        code,
        Profile::AdaptiveRIF,
        CircuitEntryPoint::EntryPoint,
        CircuitGenerationMethod::Static,
        default_test_tracer_config(),
    )
}

fn circuit_err(
    code: &str,
    entry: CircuitEntryPoint,
    method: CircuitGenerationMethod,
    tracer_config: TracerConfig,
) -> Vec<Error> {
    let profile = if method == CircuitGenerationMethod::Static {
        Profile::AdaptiveRIF
    } else {
        Profile::Unrestricted
    };

    circuit_with_options(code, profile, entry, method, tracer_config)
        .expect_err("circuit generation should fail")
}

fn circuit_with_profile_both_ways(
    code: &str,
    entry: CircuitEntryPoint,
    profile: Profile,
) -> String {
    let eval_circ = circuit_with_options_success(
        code,
        profile,
        entry.clone(),
        CircuitGenerationMethod::ClassicalEval,
        default_test_tracer_config(),
    );

    let static_circ = circuit_with_options_success(
        code,
        profile,
        entry,
        CircuitGenerationMethod::Static,
        default_test_tracer_config(),
    );

    format!("Eval:\n{eval_circ}\nStatic:\n{static_circ}")
}

pub(crate) fn circuit_with_options_success(
    code: &str,
    profile: Profile,
    entry: CircuitEntryPoint,
    method: CircuitGenerationMethod,
    config: TracerConfig,
) -> Circuit {
    circuit_with_options(code, profile, entry, method, config)
        .expect("circuit generation should succeed")
}

fn circuit_with_options(
    code: &str,
    profile: Profile,
    entry: CircuitEntryPoint,
    method: CircuitGenerationMethod,
    config: TracerConfig,
) -> Result<Circuit, Vec<Error>> {
    let mut interpreter = interpreter(code, PackageType::Exe, profile);
    interpreter.set_quantum_seed(Some(2));
    interpreter.circuit(entry, method, config)
}

pub(crate) fn default_test_tracer_config() -> TracerConfig {
    TracerConfig {
        max_operations: TracerConfig::DEFAULT_MAX_OPERATIONS,
        source_locations: true,
        group_by_scope: true,
        prune_classical_qubits: false,
    }
}

// Drives a SyntheticEntry-routed callable+args shape through the Static circuit
// path and asserts that it matches the ClassicalEval circuit. A captureless
// callable value (`AllH`) passed as an argument routes through
// `prepare_codegen_fir_from_callable_args` -> `CallableArgsBackend::SyntheticEntry`,
// exercising the `static_circuit_from_callable` SyntheticEntry arm. Parity is
// asserted with `source_locations: false` because qubit-allocation source spans
// are not currently represented in the Static method (matching the existing
// `circuit_without_groups` harness constraint).
#[test]
fn static_circuit_from_callable_with_callable_arg_matches_classical_eval() {
    let code = r#"
        namespace Test {
            operation InvokeWithQubits(nQubits : Int, f : Qubit[] => Unit) : Unit {
                use qs = Qubit[nQubits];
                f(qs);
            }
            operation AllH(qs : Qubit[]) : Unit {
                for q in qs {
                    H(q);
                }
            }
        }
    "#;
    let mut interp = interpreter(code, PackageType::Lib, Profile::AdaptiveRIF);
    interp.set_quantum_seed(Some(2));

    let globals = interp.source_globals();
    let invoke = globals
        .iter()
        .find(|(_, n, _)| &**n == "InvokeWithQubits")
        .expect("InvokeWithQubits should be a source global")
        .2
        .clone();
    let all_h = globals
        .iter()
        .find(|(_, n, _)| &**n == "AllH")
        .expect("AllH should be a source global")
        .2
        .clone();
    let args = Value::Tuple(vec![Value::Int(3), all_h].into(), None);

    let cfg = TracerConfig {
        group_by_scope: false,
        source_locations: false,
        ..default_test_tracer_config()
    };

    let static_circ = interp
        .circuit(
            CircuitEntryPoint::Callable(invoke.clone(), args.clone()),
            CircuitGenerationMethod::Static,
            cfg,
        )
        .expect("static circuit generation should succeed");
    let eval_circ = interp
        .circuit(
            CircuitEntryPoint::Callable(invoke, args),
            CircuitGenerationMethod::ClassicalEval,
            cfg,
        )
        .expect("classical-eval circuit generation should succeed");

    // Regression lock: the Static path must consume the synthetic entry so
    // its circuit matches the ClassicalEval circuit for SyntheticEntry-routed args.
    assert_eq!(static_circ.to_string(), eval_circ.to_string());

    expect![[r#"
        q_0    ── H ──
        q_1    ── H ──
        q_2    ── H ──
    "#]]
    .assert_eq(&static_circ.to_string());
}

#[test]
fn empty() {
    let circ = circuit_without_groups(
        r#"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    Message("hi");
                }
            }
        "#,
        CircuitEntryPoint::EntryPoint,
    );

    expect![""].assert_eq(&circ);
}

#[test]
fn one_gate() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    H(q);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ H@test.qs:5:20 ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn measure_same_qubit_twice() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Result[] {
                    use q = Qubit();
                    H(q);
                    let r1 = M(q);
                    let r2 = M(q);
                    [r1, r2]
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ H@test.qs:5:20 ─── M@test.qs:6:29 ─── M@test.qs:7:29 ──
                                                     ╘══════════════════╪═════════
                                                                        ╘═════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn toffoli() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit[3];
                    CCNOT(q[0], q[1], q[2]);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ──────── ● ────────
        q_1@test.qs:4:20 ──────── ● ────────
        q_2@test.qs:4:20 ─ X@test.qs:5:20 ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn rotation_gate() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    Rx(Microsoft.Quantum.Math.PI()/2.0, q);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ Rx(1.5708)@test.qs:5:20 ─
    "#]]
    .assert_eq(&circ);
}

#[test]
fn grouping_nested_callables() {
    let circ = circuit_with_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    Foo(q);
                    MResetZ(q);
                }

                operation Foo(q: Qubit) : Unit {
                    H(q);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ Main[1] ─
                              ╘═════

        [1] Main:
            q_0@test.qs:4:20 ─ [ [Foo@test.qs:5:20] ─── H@test.qs:10:20 ─── ] ─── M@test.qs:6:20 ──── |0〉@test.qs:6:20 ───
                                                                                         ╘════════════════════════════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn classical_for_loop_is_grouped() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    for i in 0..2 {
                        Foo(q);
                    }
                }

                operation Foo(q: Qubit) : Unit {
                    X(q);
                    Y(q);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ X@test.qs:11:20 ── Y@test.qs:12:20 ── X@test.qs:11:20 ── Y@test.qs:12:20 ── X@test.qs:11:20 ── Y@test.qs:12:20 ─
    "#]]
    .assert_eq(&circ);
}

#[test]
fn dynamic_for_loop_is_grouped() {
    let circ = circuit_with_options_success(
        r"
            operation Main() : Unit {
               use qubit = Qubit();
                repeat {
                    H(qubit);
                } until M(qubit) == Zero
                fixup {
                    Reset(qubit);
                }
            }
        ",
        Profile::Unrestricted,
        CircuitEntryPoint::EntryPoint,
        CircuitGenerationMethod::Simulate,
        TracerConfig {
            max_operations: 1000,
            source_locations: true,
            group_by_scope: true,
            prune_classical_qubits: false,
        },
    );

    let circ = circ.display_with_groups().to_string();

    expect![[r#"
        q_0@test.qs:2:15 ─ Main[1] ─
                              ╘═════
                              ╘═════
                              ╘═════
                              ╘═════

        [1] Main:
            q_0@test.qs:2:15 ─ loop: M(qubit) == Zero@test.qs:3:16[2] ──
                                                  ╘═════════════════════
                                                  ╘═════════════════════
                                                  ╘═════════════════════
                                                  ╘═════════════════════

        [2] loop: M(qubit) == Zero:
            q_0@test.qs:2:15 ─ (1)@test.qs:3:23[3] ── (2)@test.qs:3:23[4] ── (3)@test.qs:3:23[5] ── (4)@test.qs:3:23[6] ─
                                        ╘══════════════════════┆══════════════════════┆══════════════════════┆═══════════
                                                               ╘══════════════════════┆══════════════════════┆═══════════
                                                                                      ╘══════════════════════┆═══════════
                                                                                                             ╘═══════════

        [3] (1):
            q_0@test.qs:2:15 ─ H@test.qs:4:20 ─── M@test.qs:5:24 ──── |0〉@test.qs:7:20 ───
                                                         ╘════════════════════════════════




        [4] (2):
            q_0@test.qs:2:15 ─ H@test.qs:4:20 ─── M@test.qs:5:24 ──── |0〉@test.qs:7:20 ───
                                                         │
                                                         ╘════════════════════════════════



        [5] (3):
            q_0@test.qs:2:15 ─ H@test.qs:4:20 ─── M@test.qs:5:24 ──── |0〉@test.qs:7:20 ───
                                                         │
                                                         │
                                                         ╘════════════════════════════════


        [6] (4):
            q_0@test.qs:2:15 ─ H@test.qs:4:20 ─── M@test.qs:5:24 ──
                                                         │
                                                         │
                                                         │
                                                         ╘═════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn repeat_until_loop_is_grouped() {
    let circ = circuit_with_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    mutable i = 0;
                    repeat {
                        Foo(q);
                    } until i == 2
                    fixup {
                        set i += 1;
                    }
                }

                operation Foo(q: Qubit) : Unit {
                    X(q);
                    Y(q);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ [ [Main] ─── [ [loop: i == 2@test.qs:6:20] ── [ [(1)@test.qs:6:27] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:15:20 ── Y@test.qs:16:20 ─── ] ──── ] ─── [ [(2)@test.qs:6:27] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:15:20 ── Y@test.qs:16:20 ─── ] ──── ] ─── [ [(3)@test.qs:6:27] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:15:20 ── Y@test.qs:16:20 ─── ] ──── ] ──── ] ──── ] ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn while_loop_is_grouped() {
    let circ = circuit_with_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    mutable i = 0;
                    while (i < 2) {
                        Foo(q);
                        set i += 1;
                    }
                }

                operation Foo(q: Qubit) : Unit {
                    X(q);
                    Y(q);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ [ [Main] ─── [ [loop: i < 2@test.qs:6:20] ─── [ [(1)@test.qs:6:34] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:13:20 ── Y@test.qs:14:20 ─── ] ──── ] ─── [ [(2)@test.qs:6:34] ─── [ [Foo@test.qs:7:24] ─── X@test.qs:13:20 ── Y@test.qs:14:20 ─── ] ──── ] ──── ] ──── ] ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn loop_single_iteration_is_not_grouped() {
    let circ = circuit_with_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    for i in 0..0 {
                        Foo(q);
                    }
                }

                operation Foo(q: Qubit) : Unit {
                    X(q);
                    Y(q);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ [ [Main] ─── [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ── Y@test.qs:12:20 ─── ] ──── ] ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn loop_vertical_is_not_grouped() {
    let circ = circuit_with_groups(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use qs = Qubit[6];
                    for i in 0..5 {
                        Foo(qs[i]);
                    }
                }

                operation Foo(q: Qubit) : Unit {
                    X(q);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20 ─ Main[1] ─
                              ┆
        q_1@test.qs:4:20 ─ Main[1] ─
                              ┆
        q_2@test.qs:4:20 ─ Main[1] ─
                              ┆
        q_3@test.qs:4:20 ─ Main[1] ─
                              ┆
        q_4@test.qs:4:20 ─ Main[1] ─
                              ┆
        q_5@test.qs:4:20 ─ Main[1] ─

        [1] Main:
            q_0@test.qs:4:20 ─ [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──
            q_1@test.qs:4:20 ─ [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──
            q_2@test.qs:4:20 ─ [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──
            q_3@test.qs:4:20 ─ [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──
            q_4@test.qs:4:20 ─ [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──
            q_5@test.qs:4:20 ─ [ [Foo@test.qs:6:24] ─── X@test.qs:11:20 ─── ] ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn for_loop_nested() {
    let circ = circuit_with_options(
        r"
            namespace Test {
                @EntryPoint()
                operation Main() : Unit {
                    use qs = Qubit[3];
                    for j in 0..2 {
                        for i in 0..2 {
                            Foo(qs[i]);
                        }
                    }
                }

                operation Foo(q: Qubit) : Unit {
                    X(q);
                }
            }
        ",
        Profile::Unrestricted,
        CircuitEntryPoint::EntryPoint,
        CircuitGenerationMethod::ClassicalEval,
        TracerConfig {
            max_operations: 1000,
            source_locations: true,
            group_by_scope: true,
            prune_classical_qubits: false,
        },
    )
    .expect("circuit generation should succeed");

    let circ = circ.display_with_groups().to_string();

    expect![[r#"
        q_0@test.qs:4:20 ─ Main[1] ─
                              ┆
        q_1@test.qs:4:20 ─ Main[1] ─
                              ┆
        q_2@test.qs:4:20 ─ Main[1] ─

        [1] Main:
            q_0@test.qs:4:20 ─ loop: 0..2@test.qs:5:20[2] ──
                                            ┆
            q_1@test.qs:4:20 ─ loop: 0..2@test.qs:5:20[2] ──
                                            ┆
            q_2@test.qs:4:20 ─ loop: 0..2@test.qs:5:20[2] ──

        [2] loop: 0..2:
            q_0@test.qs:4:20 ─ (1)@test.qs:5:34[3] ── (2)@test.qs:5:34[4] ── (3)@test.qs:5:34[5] ─
                                        ┆                      ┆                      ┆
            q_1@test.qs:4:20 ─ (1)@test.qs:5:34[3] ── (2)@test.qs:5:34[4] ── (3)@test.qs:5:34[5] ─
                                        ┆                      ┆                      ┆
            q_2@test.qs:4:20 ─ (1)@test.qs:5:34[3] ── (2)@test.qs:5:34[4] ── (3)@test.qs:5:34[5] ─

        [3] (1):
            q_0@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──
            q_1@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──
            q_2@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──

        [4] (2):
            q_0@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──
            q_1@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──
            q_2@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──

        [5] (3):
            q_0@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──
            q_1@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──
            q_2@test.qs:4:20 ─ [ [Foo@test.qs:7:28] ─── X@test.qs:13:20 ─── ] ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn m_base_profile() {
    let circ = circuit_with_profile_both_ways(
        r"
            namespace Test {
                import Std.Measurement.*;
                @EntryPoint()
                operation Main() : Result[] {
                    use q = Qubit();
                    H(q);
                    [M(q)]
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
        Profile::Base,
    );

    expect![[r#"
        Eval:
        q_0@test.qs:5:20 ─ H@test.qs:6:20 ─── M@test.qs:7:21 ──
                                                     ╘═════════

        Static:
        q_0    ─ H@test.qs:6:20 ─── M@test.qs:7:21 ──
                                           ╘═════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn m_default_profile() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                import Std.Measurement.*;
                @EntryPoint()
                operation Main() : Result[] {
                    use q = Qubit();
                    H(q);
                    [M(q)]
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:5:20 ─ H@test.qs:6:20 ─── M@test.qs:7:21 ──
                                                     ╘═════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn mresetz_unrestricted_profile() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                import Std.Measurement.*;
                @EntryPoint()
                operation Main() : Result[] {
                    use q = Qubit();
                    H(q);
                    [MResetZ(q)]
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:5:20 ─ H@test.qs:6:20 ─── M@test.qs:7:21 ──── |0〉@test.qs:7:21 ───
                                                     ╘════════════════════════════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn mresetz_base_profile() {
    let circ = circuit_with_profile_both_ways(
        r"
            namespace Test {
                import Std.Measurement.*;
                @EntryPoint()
                operation Main() : Result[] {
                    use q = Qubit();
                    H(q);
                    [MResetZ(q)]
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
        Profile::Base,
    );

    // code gen in Base turns the MResetZ into an M
    expect![[r#"
        Eval:
        q_0@test.qs:5:20 ─ H@test.qs:6:20 ─── M@test.qs:7:21 ──── |0〉@test.qs:7:21 ───
                                                     ╘════════════════════════════════

        Static:
        q_0    ─ H@test.qs:6:20 ─── M@test.qs:7:21 ──
                                           ╘═════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn qubit_relabel() {
    let circ = circuit_without_groups(
        "
        namespace Test {
            operation Main() : Unit {
                use (q1, q2) = (Qubit(), Qubit());
                H(q1);
                CNOT(q1, q2);
                Relabel([q1, q2], [q2, q1]);
                H(q1);
                CNOT(q1, q2);
                MResetZ(q1);
                MResetZ(q2);
            }
        }
    ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:3:32 ─ H@test.qs:4:16 ────────── ● ──────────────────────────── X@test.qs:8:16 ─── M@test.qs:10:16 ─── |0〉@test.qs:10:16 ──
                                                     │                                     │                  ╘════════════════════════════════
        q_1@test.qs:3:41 ──────────────────── X@test.qs:5:16 ─── H@test.qs:7:16 ────────── ● ───────── M@test.qs:9:16 ──── |0〉@test.qs:9:16 ───
                                                                                                              ╘════════════════════════════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn qubit_reuse() {
    let circ = circuit_without_groups(
        "
        namespace Test {
            operation Main() : Unit {
                {
                    use q1 = Qubit();
                    X(q1);
                    MResetZ(q1);
                }
                {
                    use q2 = Qubit();
                    Y(q2);
                    MResetZ(q2);
                }
            }
        }
    ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20, test.qs:9:20 ─ X@test.qs:5:20 ─── M@test.qs:6:20 ──── |0〉@test.qs:6:20 ──── Y@test.qs:10:20 ── M@test.qs:11:20 ─── |0〉@test.qs:11:20 ──
                                                                   ╘════════════════════════════════════════════════════════════╪════════════════════════════════
                                                                                                                                ╘════════════════════════════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn qubit_reuse_no_measurements() {
    let circ = circuit_without_groups(
        "
        namespace Test {
            operation Main() : Unit {
                {
                    use q1 = Qubit();
                    X(q1);
                    Reset(q1);
                }
                {
                    use q2 = Qubit();
                    Y(q2);
                    Reset(q2);
                }
            }
        }
    ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:4:20, test.qs:9:20 ─ X@test.qs:5:20 ──── |0〉@test.qs:6:20 ──── Y@test.qs:10:20 ─── |0〉@test.qs:11:20 ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn eval_method_result_comparison() {
    let mut interpreter = interpreter_with_circuit_trace(
        r"
            namespace Test {
                import Std.Measurement.*;
                @EntryPoint()
                operation Main() : Result[] {
                    use q1 = Qubit();
                    use q2 = Qubit();
                    H(q1);
                    H(q2);
                    let r1 = M(q1);
                    let r2 = M(q2);
                    if (r1 == r2) {
                        X(q1);
                    }
                    ResetAll([q1, q2]);
                    [r1, r2]
                }
            }
        ",
        Profile::Unrestricted,
    );

    interpreter.set_quantum_seed(Some(2));

    let circuit_err = interpreter
        .circuit(
            CircuitEntryPoint::EntryPoint,
            CircuitGenerationMethod::ClassicalEval,
            default_test_tracer_config(),
        )
        .expect_err("circuit should return error")
        .pop()
        .expect("error should exist");

    expect!["Qsc.Eval.ResultComparisonUnsupported"].assert_eq(
        &circuit_err
            .code()
            .expect("error code should exist")
            .to_string(),
    );

    let circuit = interpreter.get_circuit();
    expect![""].assert_eq(&circuit.to_string());

    let mut out = std::io::sink();
    let mut r = GenericReceiver::new(&mut out);

    // Result comparisons are okay when tracing
    // circuit with the simulator.
    let circ = interpreter
        .circuit(
            CircuitEntryPoint::EntryPoint,
            CircuitGenerationMethod::Simulate,
            default_test_tracer_config(),
        )
        .expect("circuit generation should succeed");

    expect![[r#"
        q_0@test.qs:5:20 ─ H@test.qs:7:20 ─── M@test.qs:9:29 ───── X@test.qs:12:24 ───── |0〉@test.qs:14:20 ──
                                                     ╘═══════════════════════════════════════════════════════
        q_1@test.qs:6:20 ─ H@test.qs:8:20 ─── M@test.qs:10:29 ─── |0〉@test.qs:14:20 ─────────────────────────
                                                     ╘═══════════════════════════════════════════════════════
    "#]]
    .assert_eq(&circ.to_string());

    // Result comparisons are also okay if calling
    // get_circuit() after incremental evaluation,
    // because we're using the current simulator
    // state.
    interpreter
        .eval_fragments(&mut r, "Test.Main();")
        .expect("eval should succeed");

    let circuit = interpreter.get_circuit();
    expect![[r#"
        q_0@test.qs:5:20 ─ H@test.qs:7:20 ─── M@test.qs:9:29 ───── X@test.qs:12:24 ───── |0〉@test.qs:14:20 ──
                                                     ╘═══════════════════════════════════════════════════════
        q_1@test.qs:6:20 ─ H@test.qs:8:20 ─── M@test.qs:10:29 ─── |0〉@test.qs:14:20 ─────────────────────────
                                                     ╘═══════════════════════════════════════════════════════
    "#]]
    .assert_eq(&circuit.to_string());
}

#[test]
fn custom_intrinsic() {
    let circ = circuit_without_groups(
        r"
    namespace Test {
        operation foo(q: Qubit): Unit {
            body intrinsic;
        }

        @EntryPoint()
        operation Main() : Unit {
            use q = Qubit();
            foo(q);
        }
    }",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:8:12 ─ foo@test.qs:9:12 ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn custom_intrinsic_classical_arg() {
    let circ = circuit_without_groups(
        r"
    namespace Test {
        operation foo(n: Int): Unit {
            body intrinsic;
        }

        @EntryPoint()
        operation Main() : Unit {
            use q = Qubit();
            X(q);
            foo(4);
        }
    }",
        CircuitEntryPoint::EntryPoint,
    );

    // A custom intrinsic that doesn't take qubits just doesn't
    // show up on the circuit.
    expect![[r#"
        q_0@test.qs:8:12 ─ X@test.qs:9:12 ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn custom_intrinsic_one_classical_arg() {
    let circ = circuit_without_groups(
        r"
    namespace Test {
        operation foo(n: Int, q: Qubit): Unit {
            body intrinsic;
        }

        @EntryPoint()
        operation Main() : Unit {
            use q = Qubit();
            X(q);
            foo(4, q);
        }
    }",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:8:12 ─ X@test.qs:9:12 ─── foo(4)@test.qs:10:12 ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn custom_intrinsic_no_qubit_args() {
    let circ = circuit_without_groups(
        r"
    namespace Test {
        operation foo(n: Int): Unit {
            body intrinsic;
        }

        @EntryPoint()
        operation Main() : Unit {
            use q = Qubit();
            X(q);
            foo(4);
        }
    }",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:8:12 ─ X@test.qs:9:12 ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn custom_intrinsic_mixed_args_classical_eval() {
    let circ = circuit_with_options_success(
        r"
    namespace Test {
        import Std.ResourceEstimation.*;

        @EntryPoint()
        operation Main() : Unit {
            use qs = Qubit[10];
            AccountForEstimates(
                [
                    AuxQubitCount(1),
                    TCount(2),
                    RotationCount(3),
                    RotationDepth(4),
                    CczCount(5),
                    MeasurementCount(6),
                ],
                PSSPCLayout(),
                qs);
        }
    }",
        Profile::AdaptiveRIF,
        CircuitEntryPoint::EntryPoint,
        CircuitGenerationMethod::ClassicalEval,
        default_test_tracer_config(),
    );

    expect![[r#"
        q_0@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_1@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_2@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_3@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_4@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_5@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_6@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_7@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_8@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
                                                                         ┆
        q_9@test.qs:6:12 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1)@test.qs:7:12 ─
    "#]]
    .assert_eq(&circ.to_string());
}

#[test]
fn custom_intrinsic_mixed_args_static() {
    let circ = circuit_static(
        r"
    namespace Test {
        import Std.ResourceEstimation.*;

        @EntryPoint()
        operation Main() : Unit {
            use qs = Qubit[10];
            AccountForEstimates(
                [
                    AuxQubitCount(1),
                    TCount(2),
                    RotationCount(3),
                    RotationDepth(4),
                    CczCount(5),
                    MeasurementCount(6),
                ],
                PSSPCLayout(),
                qs);
        }
    }",
    );

    // This intrinsic never gets codegenned, so it's missing from the
    // circuit too.
    expect![[r#"
        q_0
        q_1
        q_2
        q_3
        q_4
        q_5
        q_6
        q_7
        q_8
        q_9
    "#]]
    .assert_eq(&circ.to_string());
}

#[test]
fn custom_intrinsic_apply_idle_noise_classical_eval() {
    let circ = circuit_with_options_success(
        r"
    namespace Test {
        import Std.Diagnostics.*;
        @EntryPoint()
        operation Main() : Unit {
            ConfigurePauliNoise(BitFlipNoise(1.0));
            use q = Qubit();
            ApplyIdleNoise(q);
        }
    }",
        Profile::AdaptiveRIF,
        CircuitEntryPoint::EntryPoint,
        CircuitGenerationMethod::ClassicalEval,
        default_test_tracer_config(),
    );

    expect![[r#"
        q_0@test.qs:6:12 ─ ApplyIdleNoise@test.qs:7:12 ─
    "#]]
    .assert_eq(&circ.to_string());
}

#[test]
fn custom_intrinsic_apply_idle_noise_static() {
    let circ = circuit_static(
        r"
    namespace Test {
        import Std.Diagnostics.*;
        @EntryPoint()
        operation Main() : Unit {
            ConfigurePauliNoise(BitFlipNoise(1.0));
            use q = Qubit();
            ApplyIdleNoise(q);
        }
    }",
    );

    // These intrinsics never get codegenned, so they're missing from the
    // circuit too.
    expect![[r#"
        q_0
    "#]]
    .assert_eq(&circ.to_string());
}

#[test]
fn operation_with_qubits() {
    let circ = circuit_without_groups(
        r"
        namespace Test {
            @EntryPoint()
            operation Main() : Result[] { [] }

            operation Test(q1: Qubit, q2: Qubit) : Result[] {
                H(q1);
                CNOT(q1, q2);
                [M(q1), M(q2)]
            }

        }",
        CircuitEntryPoint::Operation("Test.Test".into()),
    );

    expect![[r#"
        q_0@test.qs:5:27 ─ H@test.qs:6:16 ────────── ● ───────── M@test.qs:8:17 ──
                                                     │                  ╘═════════
        q_1@test.qs:5:38 ──────────────────── X@test.qs:7:16 ─── M@test.qs:8:24 ──
                                                                        ╘═════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn operation_with_qubit_arrays() {
    let circ = circuit_without_groups(
        r"
        namespace Test {
            @EntryPoint()
            operation Main() : Result[] { [] }

            import Std.Measurement.*;
            operation Test(q1: Qubit[], q2: Qubit[][], q3: Qubit[][][], q: Qubit) : Result[] {
                for q in q1 {
                    H(q);
                }
                for qs in q2 {
                    for q in qs {
                        X(q);
                    }
                }
                for qss in q3 {
                    for qs in qss {
                        for q in qs {
                            Y(q);
                        }
                    }
                }
                X(q);
                MeasureEachZ(q1)
            }
        }",
        CircuitEntryPoint::Operation("Test.Test".into()),
    );

    expect![[r#"
        q_0@test.qs:6:27   ─ H@test.qs:8:20 ─── M@test.qs:23:16 ─
                                                       ╘═════════
        q_1@test.qs:6:27   ─ H@test.qs:8:20 ─── M@test.qs:23:16 ─
                                                       ╘═════════
        q_2@test.qs:6:40   ─ X@test.qs:12:24 ────────────────────
        q_3@test.qs:6:40   ─ X@test.qs:12:24 ────────────────────
        q_4@test.qs:6:40   ─ X@test.qs:12:24 ────────────────────
        q_5@test.qs:6:40   ─ X@test.qs:12:24 ────────────────────
        q_6@test.qs:6:55   ─ Y@test.qs:18:28 ────────────────────
        q_7@test.qs:6:55   ─ Y@test.qs:18:28 ────────────────────
        q_8@test.qs:6:55   ─ Y@test.qs:18:28 ────────────────────
        q_9@test.qs:6:55   ─ Y@test.qs:18:28 ────────────────────
        q_10@test.qs:6:55  ─ Y@test.qs:18:28 ────────────────────
        q_11@test.qs:6:55  ─ Y@test.qs:18:28 ────────────────────
        q_12@test.qs:6:55  ─ Y@test.qs:18:28 ────────────────────
        q_13@test.qs:6:55  ─ Y@test.qs:18:28 ────────────────────
        q_14@test.qs:6:72  ─ X@test.qs:22:16 ────────────────────
    "#]]
    .assert_eq(&circ);
}

#[test]
fn adjoint_operation() {
    let circ = circuit_without_groups(
        r"
        namespace Test {
            @EntryPoint()
            operation Main() : Result[] { [] }

            operation Foo (q : Qubit) : Unit
                is Adj + Ctl {

                body (...) {
                    X(q);
                }

                adjoint (...) {
                    Y(q);
                }

                controlled (cs, ...) {
                }
            }

        }",
        CircuitEntryPoint::Operation("Adjoint Test.Foo".into()),
    );

    expect![[r#"
        q_0@test.qs:5:27 ─ Y@test.qs:13:20 ─
    "#]]
    .assert_eq(&circ);
}

#[test]
fn lambda() {
    let circ = circuit_without_groups(
        r"
        namespace Test {
            @EntryPoint()
            operation Main() : Result[] { [] }
        }",
        CircuitEntryPoint::Operation("q => H(q)".into()),
    );

    expect![[r#"
        q_0@line_0:0:0 ─ H@<entry>:2:18 ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn controlled_operation() {
    let circ_err = circuit_err(
        r"
        namespace Test {
            @EntryPoint()
            operation Main() : Result[] { [] }

            operation SWAP (q1 : Qubit, q2 : Qubit) : Unit
                is Adj + Ctl {

                body (...) {
                    CNOT(q1, q2);
                    CNOT(q2, q1);
                    CNOT(q1, q2);
                }

                adjoint (...) {
                    SWAP(q1, q2);
                }

                controlled (cs, ...) {
                    CNOT(q1, q2);
                    Controlled CNOT(cs, (q2, q1));
                    CNOT(q1, q2);
                }
            }

        }",
        CircuitEntryPoint::Operation("Controlled Test.SWAP".into()),
        CircuitGenerationMethod::ClassicalEval,
        default_test_tracer_config(),
    );

    // Controlled operations are not supported at the moment.
    // We don't generate an accurate call signature with the tuple arguments.
    expect![[r"
        [
            Circuit(
                ControlledUnsupported,
            ),
        ]
    "]]
    .assert_debug_eq(&circ_err);
}

#[test]
fn internal_operation() {
    let circ = circuit_without_groups(
        r"
        namespace Test {
            @EntryPoint()
            operation Main() : Result[] { [] }

            internal operation Test(q1: Qubit, q2: Qubit) : Result[] {
                H(q1);
                CNOT(q1, q2);
                [M(q1), M(q2)]
            }
        }",
        CircuitEntryPoint::Operation("Test.Test".into()),
    );

    expect![[r#"
        q_0@test.qs:5:36 ─ H@test.qs:6:16 ────────── ● ───────── M@test.qs:8:17 ──
                                                     │                  ╘═════════
        q_1@test.qs:5:47 ──────────────────── X@test.qs:7:16 ─── M@test.qs:8:24 ──
                                                                        ╘═════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn operation_with_non_qubit_args() {
    let circ_err = circuit_err(
        r"
        namespace Test {
            @EntryPoint()
            operation Main() : Result[] { [] }

            operation Test(q1: Qubit, q2: Qubit, i: Int) : Unit {
            }

        }",
        CircuitEntryPoint::Operation("Test.Test".into()),
        CircuitGenerationMethod::ClassicalEval,
        default_test_tracer_config(),
    );

    expect![[r"
        [
            Circuit(
                NoQubitParameters,
            ),
        ]
    "]]
    .assert_debug_eq(&circ_err);
}

#[test]
fn operation_with_long_gates_properly_aligned() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                import Std.Measurement.*;

                @EntryPoint()
                operation Main() : Result[] {
                    use q0 = Qubit();
                    use q1 = Qubit();

                    H(q0);
                    H(q1);
                    X(q1);
                    Ry(1.0, q1);
                    CNOT(q0, q1);
                    M(q0);

                    use q2 = Qubit();

                    H(q2);
                    Rx(1.0, q2);
                    H(q2);
                    Rx(1.0, q2);
                    H(q2);
                    Rx(1.0, q2);

                    use q3 = Qubit();

                    Rxx(1.0, q1, q3);

                    CNOT(q0, q3);

                    [M(q1), M(q3)]
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:6:20   ─ H@test.qs:9:20 ───────────────────────────────────────────────────────────────────────── ● ────────────── M@test.qs:14:20 ─────────────────────────────────────────────────────────────────── ● ───────────────────────────
                                                                                                                      │                       ╘════════════════════════════════════════════════════════════════════════════╪════════════════════════════
        q_1@test.qs:7:20   ─ H@test.qs:10:20 ─────── X@test.qs:11:20 ─────── Ry(1.0000)@test.qs:12:20 ──────── X@test.qs:13:20 ─────────────────────────────────────────────────────── Rxx(1.0000)@test.qs:27:20 ──────────┼────────── M@test.qs:31:21 ─
                                                                                                                                                                                                   ┆                       │                  ╘═════════
        q_2@test.qs:16:20  ─ H@test.qs:18:20 ── Rx(1.0000)@test.qs:19:20 ──────── H@test.qs:20:20 ─────── Rx(1.0000)@test.qs:21:20 ─── H@test.qs:22:20 ── Rx(1.0000)@test.qs:23:20 ────────────────┆───────────────────────┼────────────────────────────
        q_3@test.qs:25:20  ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Rxx(1.0000)@test.qs:27:20 ── X@test.qs:29:20 ── M@test.qs:31:28 ─
                                                                                                                                                                                                                                              ╘═════════
    "#]]
    .assert_eq(&circ);
}

#[test]
fn operation_with_subsequent_qubits_gets_horizontal_lines() {
    let circ = circuit_without_groups(
        r"
            namespace Test {
                import Std.Measurement.*;

                @EntryPoint()
                operation Main() : Unit {
                    use q0 = Qubit();
                    use q1 = Qubit();
                    Rxx(1.0, q0, q1);

                    use q2 = Qubit();
                    use q3 = Qubit();
                    Rxx(1.0, q2, q3);
                }
            }
        ",
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0@test.qs:6:20   ─ Rxx(1.0000)@test.qs:8:20 ──
                                         ┆
        q_1@test.qs:7:20   ─ Rxx(1.0000)@test.qs:8:20 ──
        q_2@test.qs:10:20  ─ Rxx(1.0000)@test.qs:12:20 ─
                                         ┆
        q_3@test.qs:11:20  ─ Rxx(1.0000)@test.qs:12:20 ─
    "#]]
    .assert_eq(&circ);
}

#[test]
fn operation_with_subsequent_qubits_no_double_rows() {
    let circ = circuit_static(
        r"
            namespace Test {
                import Std.Measurement.*;

                @EntryPoint()
                operation Main() : Unit {
                    use q0 = Qubit();
                    use q1 = Qubit();
                    Rxx(1.0, q0, q1);
                    Rxx(1.0, q0, q1);
                }
            }
        ",
    );

    expect![[r#"
        q_0    ─ Rxx(1.0000)@test.qs:8:20 ─── Rxx(1.0000)@test.qs:9:20 ──
                             ┆                            ┆
        q_1    ─ Rxx(1.0000)@test.qs:8:20 ─── Rxx(1.0000)@test.qs:9:20 ──
    "#]]
    .assert_eq(&circ.to_string());
}

#[test]
fn operation_with_subsequent_qubits_no_added_rows() {
    let circ = circuit_static(
        r"
            namespace Test {
                import Std.Measurement.*;

                @EntryPoint()
                operation Main() : Result[] {
                    use q0 = Qubit();
                    use q1 = Qubit();
                    Rxx(1.0, q0, q1);

                    use q2 = Qubit();
                    use q3 = Qubit();
                    Rxx(1.0, q2, q3);

                    [M(q0), M(q2)]
                }
            }
        ",
    );

    expect![[r#"
        q_0    ─ Rxx(1.0000)@test.qs:8:20 ─── M@test.qs:14:21 ─
                             ┆                       ╘═════════
        q_1    ─ Rxx(1.0000)@test.qs:8:20 ─────────────────────
        q_2    ─ Rxx(1.0000)@test.qs:12:20 ── M@test.qs:14:28 ─
                             ┆                       ╘═════════
        q_3    ─ Rxx(1.0000)@test.qs:12:20 ────────────────────
    "#]]
    .assert_eq(&circ.to_string());
}

#[test]
fn operation_declared_in_eval() {
    let mut interpreter = interpreter("", PackageType::Lib, Profile::Unrestricted);
    let mut out = std::io::sink();
    let mut r = GenericReceiver::new(&mut out);

    interpreter
        .eval_fragments(
            &mut r,
            "operation Foo() : Result { use q = Qubit(); H(q); return M(q) }",
        )
        .expect("eval should succeed");

    let c = interpreter
        .circuit(
            CircuitEntryPoint::EntryExpr("Foo()".into()),
            CircuitGenerationMethod::ClassicalEval,
            TracerConfig {
                max_operations: usize::MAX,
                source_locations: false,
                group_by_scope: true,
                ..default_test_tracer_config()
            },
        )
        .expect("circuit generation should succeed");

    expect![[r#"
        q_0    ─ Foo[1] ──
                    ╘═════

        [1] Foo:
            q_0    ── H ──── M ──
                             ╘═══
    "#]]
    .assert_eq(&c.display_with_groups().to_string());
}

#[test]
fn static_entrypoint_handles_callable_returned_from_function() {
    let circ = circuit_with_options_success(
        r#"
            namespace Test {
                operation ApplyOp(op : Qubit => Unit, q : Qubit) : Unit {
                    op(q);
                }

                function GetOp() : Qubit => Unit {
                    H
                }

                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    ApplyOp(GetOp(), q);
                }
            }
        "#,
        Profile::AdaptiveRIF,
        CircuitEntryPoint::EntryPoint,
        CircuitGenerationMethod::Static,
        TracerConfig {
            source_locations: false,
            ..default_test_tracer_config()
        },
    )
    .to_string();

    expect![[r#"
        q_0    ── H ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn grouped_scopes_use_source_name_for_specialized_direct_callables() {
    let circ = circuit_with_groups_without_source_locations(
        r#"
            namespace Test {
                operation ApplyOp(op : Qubit => Unit, q : Qubit) : Unit {
                    op(q);
                }

                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    ApplyOp(H, q);
                }
            }
        "#,
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0    ─ [ [Main] ─── [ [ApplyOp] ─── H ──── ] ──── ] ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn grouped_scopes_use_source_name_for_specialized_callable_arrays() {
    let circ = circuit_with_groups_without_source_locations(
        r#"
            namespace Test {
                operation ApplyOp(op : Qubit => Unit, q : Qubit) : Unit {
                    op(q);
                }

                @EntryPoint()
                operation Main() : Unit {
                    use q = Qubit();
                    let ops = [H, X];
                    for op in ops {
                        ApplyOp(op, q);
                    }
                }
            }
        "#,
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0    ─ [ [Main] ─── [ [loop: ops] ── [ [(1)] ── [ [ApplyOp] ─── H ──── ] ──── ] ─── [ [(2)] ── [ [ApplyOp] ─── X ──── ] ──── ] ──── ] ──── ] ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn grouped_scopes_match_for_user_defined_adjoint_specialization() {
    let circ = circuit_with_groups_without_source_locations(
        r#"
            namespace Test {
                operation EncodeAsLogicalQubit(physicalQubit : Qubit, aux : Qubit[]) : Unit is Adj {
                    ApplyToEachA(CNOT(physicalQubit, _), aux);
                }

                @EntryPoint()
                operation Main() : Unit {
                    use logicalQubit = Qubit[3];
                    EncodeAsLogicalQubit(logicalQubit[0], logicalQubit[1...]);
                    Adjoint EncodeAsLogicalQubit(logicalQubit[0], logicalQubit[1...]);
                }
            }
        "#,
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0    ─ Main[1] ─
                    ┆
        q_1    ─ Main[1] ─
                    ┆
        q_2    ─ Main[1] ─

        [1] Main:
            q_0    ─ EncodeAsLogicalQubit[2] ── EncodeAsLogicalQubit'[3] ──
                                ┆                           ┆
            q_1    ─ EncodeAsLogicalQubit[2] ── EncodeAsLogicalQubit'[3] ──
                                ┆                           ┆
            q_2    ─ EncodeAsLogicalQubit[2] ── EncodeAsLogicalQubit'[3] ──

        [2] EncodeAsLogicalQubit:
            q_0    ─ <lambda>[4] ─
                          ┆
            q_1    ─ <lambda>[4] ─
                          ┆
            q_2    ─ <lambda>[4] ─

        [3] EncodeAsLogicalQubit:
            q_0    ─ <lambda>'[5] ──
                           ┆
            q_1    ─ <lambda>'[5] ──
                           ┆
            q_2    ─ <lambda>'[5] ──

        [4] <lambda>:
            q_0    ── ● ──── ● ──
            q_1    ── X ─────┼───
            q_2    ───────── X ──

        [5] <lambda>:
            q_0    ── ● ──── ● ──
            q_1    ───┼───── X ──
            q_2    ── X ─────────
    "#]]
    .assert_eq(&circ);
}

#[test]
fn grouped_scopes_match_for_apply_operation_power_ca_lambda() {
    let circ = circuit_with_groups_without_source_locations(
        r#"
            namespace Test {
                operation U(q : Qubit) : Unit is Ctl + Adj {
                    Rz(Std.Math.PI() / 3.0, q);
                }

                @EntryPoint()
                operation Main() : Unit {
                    use state = Qubit();
                    use phase = Qubit[2];
                    let oracle = ApplyOperationPowerCA(_, qs => U(qs[0]), _);
                    ApplyQPE(oracle, [state], phase);
                }
            }
        "#,
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0    ─ Main[1] ─
                    ┆
        q_1    ─ Main[1] ─
                    ┆
        q_2    ─ Main[1] ─

        [1] Main:
            q_0    ──────── U[2] ────────────────────────────────────────────────────────────────────
                              ┆
            q_1    ── H ─── U[2] ──────── H ─────── Rz(-0.7854) ─── X ─── Rz(0.7854) ──── X ─────────
                              ┆                                     │                     │
            q_2    ── H ─── U[2] ─── Rz(-0.7854) ────────────────── ● ─────────────────── ● ──── H ──

        [2] U:
            q_0    ─ Rz(0.5236) ──── X ─── Rz(-0.5236) ─── X ─── Rz(0.5236) ──── X ─── Rz(-0.5236) ─── X ─── Rz(0.5236) ──── X ─── Rz(-0.5236) ─── X ──
            q_1    ───────────────── ● ─────────────────── ● ─────────────────── ● ─────────────────── ● ────────────────────┼─────────────────────┼───
            q_2    ───────────────────────────────────────────────────────────────────────────────────────────────────────── ● ─────────────────── ● ──
    "#]]
    .assert_eq(&circ);
}

#[test]
fn grouped_scopes_match_for_repeated_draw_random_bit_calls() {
    let circ = circuit_with_groups_without_source_locations(
        r#"
            namespace Test {
                operation DrawRandomBit() : Unit {
                    use q = Qubit();
                    H(q);
                    MResetZ(q);
                }

                @EntryPoint()
                operation Main() : Unit {
                    DrawRandomBit();
                    DrawRandomBit();
                }
            }
        "#,
        CircuitEntryPoint::EntryPoint,
    );

    expect![[r#"
        q_0    ─ Main[1] ─
                    ╘═════
                    ╘═════

        [1] Main:
            q_0    ─ DrawRandomBit[2] ─── DrawRandomBit[3] ──
                             ╘════════════════════┆══════════
                                                  ╘══════════

        [2] DrawRandomBit:
            q_0    ── H ──── M ──── |0〉 ──
                             ╘════════════


        [3] DrawRandomBit:
            q_0    ── H ──── M ──── |0〉 ──
                             │
                             ╘════════════
    "#]]
    .assert_eq(&circ);
}

/// Tests that invoke circuit generation through the debugger.
mod debugger_stepping {
    use super::Debugger;
    use crate::target::Profile;
    use expect_test::expect;
    use qsc_data_structures::language_features::LanguageFeatures;
    use qsc_data_structures::line_column::Encoding;
    use qsc_data_structures::source::SourceMap;
    use qsc_eval::{StepAction, StepResult, output::GenericReceiver};
    use std::fmt::Write;

    /// Steps through the code in the debugger and collects the
    /// circuit representation at each step.
    fn generate_circuit_steps(code: &str, profile: Profile) -> String {
        let sources = SourceMap::new([("test.qs".into(), code.into())], None);
        let (std_id, store) = crate::compile::package_store_with_stdlib(profile.into());
        let mut debugger = Debugger::new(
            sources,
            profile.into(),
            Encoding::Utf8,
            LanguageFeatures::default(),
            store,
            &[(std_id, None)],
        )
        .expect("debugger creation should succeed");

        debugger.interpreter.set_quantum_seed(Some(2));

        let mut out = std::io::sink();
        let mut r = GenericReceiver::new(&mut out);

        let mut circs = String::new();
        let mut result = debugger
            .eval_step(&mut r, &[], StepAction::In)
            .expect("step should succeed");

        write!(&mut circs, "step:\n{}", debugger.circuit()).expect("write should succeed");
        while !matches!(result, StepResult::Return(_)) {
            result = debugger
                .eval_step(&mut r, &[], StepAction::Next)
                .expect("step should succeed");

            write!(&mut circs, "step:\n{}", debugger.circuit()).expect("write should succeed");
        }
        circs
    }

    #[test]
    fn base_profile() {
        let circs = generate_circuit_steps(
            r"
                namespace Test {
                    import Std.Measurement.*;
                    @EntryPoint()
                    operation Main() : Result[] {
                        use q = Qubit();
                        H(q);
                        let r = M(q);
                        Reset(q);
                        [r]
                    }
                }
            ",
            Profile::Base,
        );

        expect![[r#"
            step:
            step:
            q_0@test.qs:5:24
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ──
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──
                                                         ╘═════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──── |0〉@test.qs:8:24 ───
                                                         ╘════════════════════════════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──── |0〉@test.qs:8:24 ───
                                                         ╘════════════════════════════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──── |0〉@test.qs:8:24 ───
                                                         ╘════════════════════════════════
        "#]]
        .assert_eq(&circs);
    }

    #[test]
    fn unrestricted_profile() {
        let circs = generate_circuit_steps(
            r"
                namespace Test {
                    import Std.Measurement.*;
                    @EntryPoint()
                    operation Main() : Result[] {
                        use q = Qubit();
                        H(q);
                        let r = M(q);
                        Reset(q);
                        [r]
                    }
                }
            ",
            Profile::Unrestricted,
        );

        expect![[r#"
            step:
            step:
            q_0@test.qs:5:24
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ──
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──
                                                         ╘═════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──── |0〉@test.qs:8:24 ───
                                                         ╘════════════════════════════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──── |0〉@test.qs:8:24 ───
                                                         ╘════════════════════════════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──── |0〉@test.qs:8:24 ───
                                                         ╘════════════════════════════════
        "#]]
        .assert_eq(&circs);
    }

    #[test]
    fn unrestricted_profile_result_comparison() {
        let circs = generate_circuit_steps(
            r"
                namespace Test {
                    import Std.Measurement.*;
                    @EntryPoint()
                    operation Main() : Result[] {
                        use q = Qubit();
                        H(q);
                        let r = M(q);
                        if (r == One) {
                            X(q);
                        }
                        [r]
                    }
                }
            ",
            Profile::Unrestricted,
        );

        // We set the random seed in the test to account for
        // the nondeterministic output. Since the debugger is running
        // the real simulator, the circuit is going to vary from run to run
        // depending on measurement outcomes.
        expect![[r#"
            step:
            step:
            q_0@test.qs:5:24
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ──
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──
                                                         ╘═════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ──
                                                         ╘═════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ─── X@test.qs:9:28 ──
                                                         ╘════════════════════════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ─── X@test.qs:9:28 ──
                                                         ╘════════════════════════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ─── X@test.qs:9:28 ──
                                                         ╘════════════════════════════
            step:
            q_0@test.qs:5:24 ─ H@test.qs:6:24 ─── M@test.qs:7:32 ─── X@test.qs:9:28 ──
                                                         ╘════════════════════════════
        "#]]
        .assert_eq(&circs);
    }
}
