use std::fs;
use std::path::{Path, PathBuf};

use debtmap::analyzers::{Analyzer, get_analyzer};
use debtmap::core::{DebtType, Language, LanguageSpecificData, PurityLevel};
use debtmap::io::walker::FileWalker;
use debtmap::utils::language_parser::{default_languages, parse_languages, parse_single_language};

fn fixture_path(relative: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/solidity")
        .join(relative)
}

fn analyze_fixture(relative: &str) -> debtmap::core::FileMetrics {
    let path = fixture_path(relative);
    let content = fs::read_to_string(&path).expect("fixture should exist");
    let analyzer = get_analyzer(Language::Solidity);
    let ast = analyzer.parse(&content, path).expect("parse fixture");
    analyzer.analyze(&ast)
}

fn analyze_source(name: &str, source: &str) -> debtmap::core::FileMetrics {
    let analyzer = debtmap::analyzers::solidity::SolidityAnalyzer::new();
    let path = PathBuf::from(name);
    let ast = analyzer.parse(source, path).expect("parse source");
    analyzer.analyze(&ast)
}

fn analyze_source_with_config(
    name: &str,
    source: &str,
    config: debtmap::config::SolidityLanguageConfig,
) -> debtmap::core::FileMetrics {
    let analyzer = debtmap::analyzers::solidity::SolidityAnalyzer::new().with_config(config);
    let path = PathBuf::from(name);
    let ast = analyzer.parse(source, path).expect("parse source");
    analyzer.analyze(&ast)
}

fn has_pattern(metrics: &debtmap::core::FileMetrics, pattern: &str) -> bool {
    metrics.debt_items.iter().any(|item| {
        matches!(
            &item.debt_type,
            DebtType::CodeSmell { smell_type } if smell_type.as_deref() == Some(pattern)
        )
    })
}

#[test]
fn test_language_parser_supports_solidity() {
    assert_eq!(parse_single_language("solidity"), Some(Language::Solidity));
    assert_eq!(parse_single_language("sol"), Some(Language::Solidity));
    assert_eq!(parse_single_language("SOL"), Some(Language::Solidity));
}

#[test]
fn test_default_languages_include_solidity() {
    let defaults = default_languages();
    assert_eq!(defaults.len(), 6);
    assert!(defaults.contains(&Language::Solidity));
}

#[test]
fn test_parse_languages_solidity_only() {
    let result = parse_languages(Some(vec!["sol".to_string()]));
    assert_eq!(result, vec![Language::Solidity]);
}

#[test]
fn test_get_analyzer_routes_to_solidity() {
    let analyzer = get_analyzer(Language::Solidity);
    assert_eq!(analyzer.language(), Language::Solidity);
}

#[test]
fn test_analyze_simple_token_fixture() {
    let metrics = analyze_fixture("simple/SimpleToken.sol");

    assert_eq!(metrics.language, Language::Solidity);
    assert!(
        metrics
            .complexity
            .functions
            .iter()
            .any(|f| f.name.contains("mint"))
    );
    assert!(metrics.dependencies.is_empty() || metrics.path.ends_with("SimpleToken.sol"));
}

#[test]
fn test_analyze_complex_vault_emits_complexity_debt() {
    let analyzer = debtmap::analyzers::solidity::SolidityAnalyzer::new().with_threshold(2);
    let path = fixture_path("complex/ComplexVault.sol");
    let content = fs::read_to_string(&path).unwrap();
    let ast = analyzer.parse(&content, path).unwrap();
    let metrics = analyzer.analyze(&ast);

    assert!(metrics.complexity.functions[0].cyclomatic > 2);
    assert!(
        metrics
            .debt_items
            .iter()
            .any(|item| { matches!(item.debt_type, debtmap::core::DebtType::Complexity { .. }) })
    );
}

#[test]
fn test_analyze_security_fixture_detects_tx_origin() {
    let metrics = analyze_fixture("security/InsecureBank.sol");
    let withdraw = metrics
        .complexity
        .functions
        .iter()
        .find(|function| function.name.contains("withdraw"))
        .expect("withdraw function");

    assert!(
        withdraw
            .detected_patterns
            .as_ref()
            .is_some_and(|patterns| patterns.contains(&"tx-origin-usage".to_string()))
    );
}

#[test]
fn test_foundry_fixture_skips_debt() {
    let metrics = analyze_fixture("foundry/Token.t.sol");
    assert!(metrics.complexity.functions.iter().all(|f| f.is_test));
    assert!(metrics.debt_items.is_empty());
}

#[test]
fn test_file_walker_discovers_sol_files() {
    let root = fixture_path("");
    let files = FileWalker::new(root)
        .with_languages(vec![Language::Solidity])
        .walk()
        .expect("walk fixtures");

    assert!(files.iter().any(|path| path.ends_with("SimpleToken.sol")));
    assert!(files.iter().any(|path| path.ends_with("Token.t.sol")));
}

#[test]
fn test_language_from_extension_sol() {
    assert_eq!(Language::from_extension("sol"), Language::Solidity);
    assert_eq!(
        Language::from_path(Path::new("contracts/Token.sol")),
        Language::Solidity
    );
}

#[test]
fn test_security_patterns_positive_and_negative_fixtures() {
    let cases = [
        (
            "tx-origin-usage",
            r#"pragma solidity 0.8.20; contract C { function f() internal view returns (bool) { return tx.origin == msg.sender; } }"#,
            r#"pragma solidity 0.8.20; contract C { function f() internal view returns (bool) { return msg.sender != address(0); } }"#,
        ),
        (
            "unchecked-low-level-call",
            r#"pragma solidity 0.8.20; contract C { function f(address target) internal { target.call{value: 0}(""); } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(address target) internal view returns (address) { return target; } }"#,
        ),
        (
            "external-call-before-state-update",
            r#"pragma solidity 0.8.20; contract C { mapping(address => uint256) balances; function f() internal { payable(msg.sender).transfer(1); balances[msg.sender] = 0; } }"#,
            r#"pragma solidity 0.8.20; contract C { mapping(address => uint256) balances; function f() internal { balances[msg.sender] = 0; payable(msg.sender).transfer(1); } }"#,
        ),
        (
            "delegatecall-usage",
            r#"pragma solidity 0.8.20; contract C { function f(address target) internal { target.delegatecall(""); } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(address target) internal view returns (address) { return target; } }"#,
        ),
        (
            "selfdestruct-usage",
            r#"pragma solidity 0.8.20; contract C { function f(address payable target) internal { selfdestruct(target); } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(address payable target) internal view returns (address) { return target; } }"#,
        ),
        (
            "assembly-block",
            r#"pragma solidity 0.8.20; contract C { function f() internal pure returns (uint256 x) { assembly { x := 1 } } }"#,
            r#"pragma solidity 0.8.20; contract C { function f() internal pure returns (uint256) { return 1; } }"#,
        ),
        (
            "unbounded-loop",
            r#"pragma solidity 0.8.20; contract C { address[] users; function f() internal { for (uint256 i = 0; i < users.length; i++) { } } }"#,
            r#"pragma solidity 0.8.20; contract C { function f() internal { for (uint256 i = 0; i < 10; i++) { } } }"#,
        ),
        (
            "missing-access-control",
            r#"pragma solidity 0.8.20; contract C { function setValue(uint256 value) public { value; } }"#,
            r#"pragma solidity 0.8.20; contract C { modifier onlyOwner() { _; } function setValue(uint256 value) public onlyOwner { value; } }"#,
        ),
        (
            "hardcoded-address",
            r#"pragma solidity 0.8.20; contract C { function f() internal pure returns (address) { return 0x1234567890123456789012345678901234567890; } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(address target) internal pure returns (address) { return target; } }"#,
        ),
        (
            "floating-pragma",
            r#"pragma solidity ^0.8.20; contract C { function f() internal {} }"#,
            r#"pragma solidity 0.8.20; contract C { function f() internal {} }"#,
        ),
    ];

    for (pattern, positive, negative) in cases {
        assert!(
            has_pattern(&analyze_source("Positive.sol", positive), pattern),
            "expected positive fixture to detect {pattern}"
        );
        assert!(
            !has_pattern(&analyze_source("Negative.sol", negative), pattern),
            "expected negative fixture not to detect {pattern}"
        );
    }
}

#[test]
fn test_security_pattern_precision_refinements() {
    let cases = [
        (
            "external-call-before-state-update",
            r#"pragma solidity 0.8.20; contract C { mapping(address => uint256) balances; function f() internal { if (true) { payable(msg.sender).transfer(1); } balances[msg.sender] = 0; } }"#,
            r#"pragma solidity 0.8.20; contract C { mapping(address => uint256) balances; function f() internal { balances[msg.sender] = 0; if (true) { payable(msg.sender).transfer(1); } } }"#,
        ),
        (
            "unchecked-low-level-call",
            r#"pragma solidity 0.8.20; contract C { function f(address target) internal { target.call{value: 0}(""); } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(address target) internal { (bool success, ) = target.call{value: 0}(""); require(success); } }"#,
        ),
        (
            "missing-access-control",
            r#"pragma solidity 0.8.20; contract C { function setValue(uint256 value) public { value; } }"#,
            r#"pragma solidity 0.8.20; contract C { function _checkOwner() internal view {} function setValue(uint256 value) public { _checkOwner(); value; } }"#,
        ),
        (
            "unbounded-loop",
            r#"pragma solidity 0.8.20; contract C { address[] users; function f() internal { for (uint256 i = 0; i < users.length; i++) {} } }"#,
            r#"pragma solidity 0.8.20; contract C { uint256[10] items; function f() internal { for (uint256 i = 0; i < items.length; i++) {} } }"#,
        ),
        (
            "hardcoded-address",
            r#"pragma solidity 0.8.20; contract C { function f() internal pure returns (address) { return 0x1234567890123456789012345678901234567890; } }"#,
            r#"pragma solidity 0.8.20; contract C { function f() internal pure returns (address) { return 0x0000000000000000000000000000000000000000; } }"#,
        ),
    ];

    for (pattern, positive, negative) in cases {
        assert!(
            has_pattern(&analyze_source("Positive.sol", positive), pattern),
            "expected positive fixture to detect {pattern}"
        );
        assert!(
            !has_pattern(&analyze_source("Negative.sol", negative), pattern),
            "expected negative fixture not to detect {pattern}"
        );
    }
}

#[test]
fn test_large_contract_pattern_positive_and_negative() {
    let source = r#"pragma solidity 0.8.20;
contract C {
    function a() internal {}
    function b() internal {}
}
"#;
    let low_threshold = debtmap::config::SolidityLanguageConfig {
        large_contract_threshold: 1,
        ..Default::default()
    };
    let high_threshold = debtmap::config::SolidityLanguageConfig {
        large_contract_threshold: 10,
        ..Default::default()
    };

    assert!(has_pattern(
        &analyze_source_with_config("Large.sol", source, low_threshold),
        "large-contract"
    ));
    assert!(!has_pattern(
        &analyze_source_with_config("Small.sol", source, high_threshold),
        "large-contract"
    ));
}

#[test]
fn test_solidity_entropy_metrics_are_populated() {
    let source = r#"pragma solidity 0.8.20;
contract C {
    function validate(address a, address b, address c) public pure {
        require(a != address(0));
        require(b != address(0));
        require(c != address(0));
    }
}
"#;
    let metrics = analyze_source("Entropy.sol", source);
    let function = metrics
        .complexity
        .functions
        .iter()
        .find(|function| function.name.contains("validate"))
        .expect("validate function");

    assert!(function.entropy_score.is_some());
    assert!(function.entropy_analysis.is_some());
    assert!(function.adjusted_complexity.is_some());
}

#[test]
fn test_advanced_advisory_patterns_positive_and_negative() {
    let cases = [
        (
            "unchecked-arithmetic",
            r#"pragma solidity 0.8.20; contract C { function f(uint256 x) internal pure returns (uint256) { unchecked { return x + 1; } } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(uint256 x) internal pure returns (uint256) { return x + 1; } }"#,
        ),
        (
            "unsafe-erc20-transfer",
            r#"pragma solidity 0.8.20; interface IERC20 { function transfer(address,uint256) external returns (bool); } contract C { function f(IERC20 token, address to) internal { token.transfer(to, 1); } }"#,
            r#"pragma solidity 0.8.20; interface IERC20 { function transfer(address,uint256) external returns (bool); } contract C { function f(IERC20 token, address to) internal { require(token.transfer(to, 1)); } }"#,
        ),
        (
            "push-without-length-cap",
            r#"pragma solidity 0.8.20; contract C { uint256[] values; function f(uint256 value) internal { values.push(value); } }"#,
            r#"pragma solidity 0.8.20; contract C { uint256[] values; function f(uint256 value) internal { require(values.length < 10); values.push(value); } }"#,
        ),
        (
            "block-timestamp-dependency",
            r#"pragma solidity 0.8.20; contract C { function f() internal view returns (uint256) { return block.timestamp; } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(uint256 nowish) internal pure returns (uint256) { return nowish; } }"#,
        ),
        (
            "tx-gas-price-dependency",
            r#"pragma solidity 0.8.20; contract C { function f() internal view returns (uint256) { return tx.gasprice; } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(uint256 gasPrice) internal pure returns (uint256) { return gasPrice; } }"#,
        ),
        (
            "encode-packed-collision",
            r#"pragma solidity 0.8.20; contract C { function f(string memory a, string memory b) internal pure returns (bytes memory) { return abi.encodePacked(a, b); } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(uint256 a, address b) internal pure returns (bytes memory) { return abi.encodePacked(a, b); } }"#,
        ),
        (
            "delegatecall-in-constructor",
            r#"pragma solidity 0.8.20; contract C { constructor(address target) { target.delegatecall(""); } }"#,
            r#"pragma solidity 0.8.20; contract C { function f(address target) internal { target.delegatecall(""); } }"#,
        ),
    ];

    for (pattern, positive, negative) in cases {
        assert!(
            has_pattern(&analyze_source("AdvancedPositive.sol", positive), pattern),
            "expected positive fixture to detect {pattern}"
        );
        assert!(
            !has_pattern(&analyze_source("AdvancedNegative.sol", negative), pattern),
            "expected negative fixture not to detect {pattern}"
        );
    }
}

#[test]
fn test_natspec_debt_for_public_function_without_docs() {
    let undocumented = analyze_source(
        "Undocumented.sol",
        r#"pragma solidity 0.8.20;
contract C {
    function f() public {}
}
"#,
    );
    let documented = analyze_source(
        "Documented.sol",
        r#"pragma solidity 0.8.20;
contract C {
    /// @notice Does the thing.
    function f() public {}
}
"#,
    );

    assert!(has_pattern(&undocumented, "missing-natspec"));
    assert!(!has_pattern(&documented, "missing-natspec"));
}

#[test]
fn test_natspec_completeness_positive_and_negative_fixtures() {
    let cases = [
        (
            "missing-natspec-param",
            r#"pragma solidity 0.8.20;
contract C {
    /// @notice Moves tokens.
    function f(address to, uint256 amount) public {}
}
"#,
            r#"pragma solidity 0.8.20;
contract C {
    /// @notice Moves tokens.
    /// @param to Recipient
    /// @param amount Amount
    function f(address to, uint256 amount) public {}
}
"#,
        ),
        (
            "stale-natspec-param",
            r#"pragma solidity 0.8.20;
contract C {
    /// @notice Moves tokens.
    /// @param stale Old param
    function f(address to) public {}
}
"#,
            r#"pragma solidity 0.8.20;
contract C {
    /// @notice Moves tokens.
    /// @param to Recipient
    function f(address to) public {}
}
"#,
        ),
        (
            "missing-natspec-return",
            r#"pragma solidity 0.8.20;
contract C {
    /// @notice Reads value.
    function f() public view returns (uint256) {}
}
"#,
            r#"pragma solidity 0.8.20;
contract C {
    /// @notice Reads value.
    /// @return The stored value
    function f() public view returns (uint256) {}
}
"#,
        ),
        (
            "placeholder-natspec",
            r#"pragma solidity 0.8.20;
contract C {
    /// @notice TODO
    function f() public {}
}
"#,
            r#"pragma solidity 0.8.20;
contract C {
    /// @notice Returns the stored value.
    function f() public {}
}
"#,
        ),
    ];

    for (pattern, positive, negative) in cases {
        assert!(
            has_pattern(&analyze_source("Positive.sol", positive), pattern),
            "expected positive fixture to detect {pattern}"
        );
        assert!(
            !has_pattern(&analyze_source("Negative.sol", negative), pattern),
            "expected negative fixture not to detect {pattern}"
        );
    }
}

#[test]
fn test_natspec_ignores_internal_functions() {
    let metrics = analyze_source(
        "Internal.sol",
        r#"pragma solidity 0.8.20;
contract C {
    function hidden(uint256 value) internal {}
}
"#,
    );

    assert!(!metrics.debt_items.iter().any(|item| {
        matches!(
            &item.debt_type,
            DebtType::CodeSmell { smell_type }
                if smell_type
                    .as_deref()
                    .is_some_and(|pattern| pattern.contains("natspec"))
        )
    }));
}

#[test]
fn test_natspec_ignores_docs_separated_too_far_from_function() {
    let metrics = analyze_source(
        "FarDoc.sol",
        r#"pragma solidity 0.8.20;
contract C {
    /// @notice Old docs far away.

    uint256 public value;

    function f() public {}
}
"#,
    );

    assert!(has_pattern(&metrics, "missing-natspec"));
}

#[test]
fn test_solidity_language_specific_data_and_purity() {
    let metrics = analyze_source(
        "Metadata.sol",
        r#"pragma solidity 0.8.20;
contract C {
    uint256 public value;
    /// @notice Reads value.
    function f() public view returns (uint256) {
        return value;
    }
}
"#,
    );
    let function = metrics
        .complexity
        .functions
        .iter()
        .find(|function| function.name == "C.f")
        .expect("function");

    assert_eq!(function.purity_level, Some(PurityLevel::ReadOnly));
    assert_eq!(function.is_pure, Some(false));
    match function.language_specific.as_ref() {
        Some(LanguageSpecificData::Solidity(data)) => {
            assert_eq!(data.state_mutability.as_deref(), Some("view"));
            assert!(data.reads_state);
            assert!(!data.writes_state);
            assert!(!data.is_payable);
        }
        other => panic!("expected Solidity language-specific data, got {other:?}"),
    }
}

#[test]
fn test_solidity_purity_effect_analysis_fixtures() {
    let state_write = analyze_source(
        "StateWrite.sol",
        r#"pragma solidity 0.8.20;
contract C {
    uint256 public value;
    function setValue(uint256 next) public { value = next; }
}
"#,
    );
    let writer = state_write
        .complexity
        .functions
        .iter()
        .find(|function| function.name == "C.setValue")
        .expect("setValue");
    assert_eq!(writer.purity_level, Some(PurityLevel::Impure));
    assert!(!has_pattern(&state_write, "mutability-mismatch"));

    let pure_mismatch = analyze_source(
        "PureMismatch.sol",
        r#"pragma solidity 0.8.20;
contract C {
    uint256 public value;
    function readAsPure() public pure returns (uint256) { return value; }
}
"#,
    );
    assert!(has_pattern(&pure_mismatch, "mutability-mismatch"));

    let external_call = analyze_source(
        "External.sol",
        r#"pragma solidity 0.8.20;
contract C {
    function payout(address payable recipient) public {
        recipient.transfer(1);
    }
}
"#,
    );
    let payout = external_call
        .complexity
        .functions
        .iter()
        .find(|function| function.name == "C.payout")
        .expect("payout");
    assert_eq!(payout.purity_level, Some(PurityLevel::Impure));

    let local_only = analyze_source(
        "Local.sol",
        r#"pragma solidity 0.8.20;
contract C {
    function locals() public pure returns (uint256) {
        uint256 local = 1;
        return local + 1;
    }
}
"#,
    );
    let locals = local_only
        .complexity
        .functions
        .iter()
        .find(|function| function.name == "C.locals")
        .expect("locals");
    assert_eq!(locals.purity_level, Some(PurityLevel::StrictlyPure));
    assert_eq!(locals.is_pure, Some(true));
}
