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

use crate::compile::preprocess::RemoveCircuitSpans;
use crate::compile::{parse_all, SourceMap};
use qsc_ast::ast::{
    Attr, CallableBody, CallableDecl, Expr, ExprKind, Ident, NodeId, Path, PathKind,
};
use qsc_ast::ast::{ItemKind, Package, TopLevelNode};
use qsc_ast::mut_visit::MutVisitor;
use qsc_data_structures::language_features::LanguageFeatures;
use qsc_data_structures::span::Span;
use std::sync::Arc;

use crate::compile::{preprocess::matches_config, TargetCapabilityFlags};

fn named_attr(name: &str) -> Attr {
    Attr {
        name: Box::new(Ident {
            name: name.into(),
            span: Span::default(),
            id: NodeId::default(),
        }),
        arg: Box::new(Expr {
            id: NodeId::default(),
            span: Span::default(),
            kind: Box::new(ExprKind::Tuple(Box::new([]))),
        }),
        span: Span::default(),
        id: NodeId::default(),
    }
}

fn name_value_attr(name: &str, value: &str) -> Attr {
    Attr {
        name: Box::new(Ident {
            name: name.into(),
            span: Span::default(),
            id: NodeId::default(),
        }),
        arg: Box::new(Expr {
            id: NodeId::default(),
            span: Span::default(),
            kind: Box::new(ExprKind::Paren(Box::new(Expr {
                id: NodeId::default(),
                span: Span::default(),
                kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path {
                    id: NodeId::default(),
                    span: Span::default(),
                    segments: None,
                    name: Box::new(Ident {
                        name: value.into(),
                        span: Span::default(),
                        id: NodeId::default(),
                    }),
                })))),
            }))),
        }),
        span: Span::default(),
        id: NodeId::default(),
    }
}

fn prepare_ast_for_circuit_tests() -> Package {
    let circuit_source = (
        Arc::from("circuit.qsc"),
        Arc::from("namespace CircuitTest { operation Circuit(qs : Qubit[]) : Unit { X(qs[0]); } }"),
    );
    let qsharp_source = (
        Arc::from("test.qs"),
        Arc::from("namespace Test { operation Main(qs : Qubit[]) : Unit { X(qs[0]); } }"),
    );
    let sources = SourceMap::new([circuit_source, qsharp_source], None);
    let (mut package, errs) = parse_all(&sources, LanguageFeatures::default());
    assert!(errs.is_empty(), "{errs:?}");
    let mut visitor = RemoveCircuitSpans::new(&sources);
    visitor.visit_package(&mut package);
    package
}

/// Helper to find a callable declaration by name in a package AST.
fn find_callable<'a>(package: &'a Package, name: &str) -> &'a CallableDecl {
    package
        .nodes
        .iter()
        .find_map(|node| {
            if let TopLevelNode::Namespace(ns) = node {
                ns.items.iter().find_map(|item| {
                    if let ItemKind::Callable(decl) = item.kind.as_ref() {
                        if decl.name.name.as_ref() == name {
                            Some(decl.as_ref())
                        } else {
                            None
                        }
                    } else {
                        None
                    }
                })
            } else {
                None
            }
        })
        .unwrap_or_else(|| panic!("{name} callable not found"))
}

#[test]
fn no_attrs_matches() {
    assert!(matches_config(&[], TargetCapabilityFlags::empty()));
}

#[test]
fn unknown_attrs_matches() {
    assert!(matches_config(
        &[Box::new(named_attr("unknown"))],
        TargetCapabilityFlags::empty()
    ));
}

#[test]
fn none_attrs_matches_empty() {
    assert!(matches_config(
        &[Box::new(name_value_attr("Config", "Base"))],
        TargetCapabilityFlags::empty()
    ));
}

#[test]
fn none_attrs_does_not_match_all() {
    assert!(!matches_config(
        &[Box::new(name_value_attr("Config", "Base"))],
        TargetCapabilityFlags::all()
    ));
}

#[test]
fn none_attrs_does_not_match_adaptive() {
    assert!(!matches_config(
        &[Box::new(name_value_attr("Config", "Base"))],
        TargetCapabilityFlags::Adaptive
    ));
}

#[test]
fn adaptive_attrs_does_not_match_empty() {
    assert!(!matches_config(
        &[Box::new(name_value_attr("Config", "Adaptive"))],
        TargetCapabilityFlags::empty()
    ));
}

#[test]
fn integercomputations_attrs_does_not_match_empty() {
    assert!(!matches_config(
        &[Box::new(name_value_attr("Config", "IntegerComputations"))],
        TargetCapabilityFlags::empty()
    ));
}

#[test]
fn floatingpointcomputations_attrs_does_not_match_empty() {
    assert!(!matches_config(
        &[Box::new(name_value_attr(
            "Config",
            "FloatingPointComputations"
        ))],
        TargetCapabilityFlags::empty()
    ));
}

#[test]
fn unrestricted_attrs_does_not_match_empty() {
    assert!(!matches_config(
        &[Box::new(name_value_attr("Config", "Unrestricted"))],
        TargetCapabilityFlags::empty()
    ));
}

#[test]
fn unrestricted_attrs_matches_all() {
    assert!(matches_config(
        &[Box::new(name_value_attr("Config", "Unrestricted"))],
        TargetCapabilityFlags::all()
    ));
}

#[test]
fn remove_circuit_spans_clears_stmt_spans_in_qsc_files() {
    let ast = prepare_ast_for_circuit_tests();
    let circuit_callable = find_callable(&ast, "Circuit");

    // Assert the callable declaration has a non-default span
    assert_ne!(
        circuit_callable.span,
        Span::default(),
        "Callable span should not be default"
    );

    // Assert that the body is a block and all statement spans inside the callable are cleared
    match circuit_callable.body.as_ref() {
        CallableBody::Block(block) => {
            for stmt in &block.stmts {
                assert_eq!(
                    stmt.span,
                    Span::default(),
                    "Statement span inside Circuit should be cleared"
                );
            }
        }
        CallableBody::Specs(_) => panic!("Expected Circuit body to be a block"),
    }
}

#[test]
fn remove_circuit_spans_does_not_clear_spans_outside_qsc_files() {
    let ast = prepare_ast_for_circuit_tests();
    let main_callable = find_callable(&ast, "Main");

    // Assert the callable declaration has a non-default span
    assert_ne!(
        main_callable.span,
        Span::default(),
        "Callable span should not be default"
    );

    // Assert that the body is a block and all statement spans inside the callable are NOT cleared
    match main_callable.body.as_ref() {
        CallableBody::Block(block) => {
            for stmt in &block.stmts {
                assert_ne!(
                    stmt.span,
                    Span::default(),
                    "Statement span inside Main should NOT be cleared"
                );
            }
        }
        CallableBody::Specs(_) => panic!("Expected Main body to be a block"),
    }
}
