use rasm_core::{
    analysis::{
        air::{Terminator, analysis::find_indirect_exits, build_cfg, err::AIRErrorKind},
        ast_analysis::{
            ASTAnalyser,
            err::{ASTAnalyseErrorKind, ASTAnalyseErrorVariant, ASTAnalyseWarningKind},
        },
    },
    lexer::Lexer,
    parser::{Parser, ast::AST},
    src::SourceCode,
    str_pool::StrPool,
};

struct Parsed {
    ast: AST,
    str_pool: StrPool,
}

fn parse(code: &str) -> Parsed {
    let source = SourceCode::new(code.to_string());
    let mut str_pool = StrPool::from_source(&source);

    let mut lexer = Lexer::new(&mut str_pool, source);
    lexer.tokenize();
    assert!(lexer.errors.is_empty(), "lexer errors: {:?}", lexer.errors);

    let mut parser = Parser::new(&lexer.tokens, &mut str_pool);
    let ast = parser.parse();
    assert!(
        parser.errors.is_empty(),
        "parser errors: {:?}",
        parser.errors
    );

    Parsed { ast, str_pool }
}

#[test]
fn ast_analyser_reports_errors_and_warnings() {
    let parsed = parse(
        r#"
            _start:
                mov rax, rax
                ret
                mov rbx, 1
            _start:
                ret
        "#,
    );

    let mut analyser = ASTAnalyser::new(&parsed.ast, &parsed.str_pool);
    analyser.analyse();

    assert!(analyser.errors.iter().any(|finding| {
        matches!(
            finding.kind,
            ASTAnalyseErrorKind::Error(ASTAnalyseErrorVariant::DuplicateLabel { .. })
        )
    }));
    assert!(analyser.errors.iter().any(|finding| {
        matches!(
            finding.kind,
            ASTAnalyseErrorKind::Warning(ASTAnalyseWarningKind::RedundantSelfAssign(_))
        )
    }));
    assert!(analyser.errors.iter().any(|finding| {
        matches!(
            finding.kind,
            ASTAnalyseErrorKind::Warning(ASTAnalyseWarningKind::DeadCodeAfterTerminator { .. })
        )
    }));
}

#[test]
fn cfg_reports_unknown_internal_call_target() {
    let parsed = parse(
        r#"
            _start:
                call missing_label
                ret
        "#,
    );

    let errors = build_cfg(&parsed.ast).expect_err("unknown call target must be reported");
    assert!(
        errors
            .iter()
            .any(|error| { matches!(error.kind, AIRErrorKind::UnknownLabel(_)) })
    );
}

#[test]
fn cfg_accepts_call_to_import_as_external_call() {
    let parsed = parse(
        r#"
            .import kernel32.dll GetStdHandle
            _start:
                call GetStdHandle
                mov rax, 0
                ret
        "#,
    );

    let cfg = build_cfg(&parsed.ast).expect("imported call target must not be an unknown label");

    assert_eq!(cfg.blocks.len(), 2);
    assert!(matches!(
        cfg.blocks[0].terminator,
        Terminator::ExternalCall { fallthrough } if fallthrough.0 == 1
    ));
    assert_eq!(cfg.blocks[1].predecessors, vec![cfg.blocks[0].id]);
    assert!(find_indirect_exits(&cfg).is_empty());
}

#[test]
fn cfg_accepts_call_to_extern_as_external_call() {
    let parsed = parse(
        r#"
            .extern host_fn
            _start:
                call host_fn
                ret
        "#,
    );

    let cfg = build_cfg(&parsed.ast).expect("extern call target must not be an unknown label");

    assert_eq!(cfg.blocks.len(), 2);
    assert!(matches!(
        cfg.blocks[0].terminator,
        Terminator::ExternalCall { fallthrough } if fallthrough.0 == 1
    ));
}
