//! Integration tests for different types of code clone detection
//!
//! Tests cover:
//! - Type-1 clones: Exact copies (only whitespace/comments differ)
//! - Type-2 clones: Parametrized clones (identifiers/literals renamed)
//! - Type-3 clones: Near-miss clones (with modifications)
//! - Extension algorithm: Variable-length duplicate detection

use dupe_core::{detect_duplicates_with_extension, normalize, Scanner};
use std::path::PathBuf;

#[test]
fn test_type1_exact_clones() {
    // Type-1: Exact copies with only whitespace differences
    let code1 = r#"
        function calculateSum(a, b) {
            return a + b;
        }
    "#;

    let code2 = r#"
        function calculateSum(a, b) {
            return a + b;
        }
    "#;

    let tokens1 = normalize(code1);
    let tokens2 = normalize(code2);

    // Type-1 clones should produce identical token sequences
    assert_eq!(tokens1, tokens2);
}

#[test]
fn test_type2_parametrized_clones() {
    // Type-2: Same structure, different identifiers
    let code1 = r#"
        function calculateTotal(items) {
            let sum = 0;
            for (let i = 0; i < items.length; i++) {
                sum += items[i].price;
            }
            return sum;
        }
    "#;

    let code2 = r#"
        function computePrice(products) {
            let total = 0;
            for (let j = 0; j < products.length; j++) {
                total += products[j].price;
            }
            return total;
        }
    "#;

    let tokens1 = normalize(code1);
    let tokens2 = normalize(code2);

    // Type-2 clones should produce identical token sequences after normalization
    // (identifiers normalized to @@ID)
    assert_eq!(tokens1, tokens2);
}

#[test]
fn test_type2_string_literals() {
    // Type-2: Same structure, different string literals
    let code1 = r#"
        function greet(name) {
            console.log("Hello, " + name);
            return "Welcome";
        }
    "#;

    let code2 = r#"
        function welcome(user) {
            console.log("Hi, " + user);
            return "Greetings";
        }
    "#;

    let tokens1 = normalize(code1);
    let tokens2 = normalize(code2);

    // String literals should be normalized
    assert_eq!(tokens1, tokens2);
}

#[test]
fn test_type3_near_miss_clones() {
    // Type-3: Similar structure with modifications
    let code1 = r#"
        function processOrder(order) {
            if (!order.id) return null;
            let total = calculateTotal(order.items);
            return total;
        }
    "#;

    let code2 = r#"
        function handleOrder(order) {
            if (!order.id) return null;
            let total = calculateTotal(order.items);
            let tax = total * 0.1;  // Additional line
            return total;
        }
    "#;

    let tokens1 = normalize(code1);
    let tokens2 = normalize(code2);

    // Type-3 clones should have partial overlap
    assert_ne!(tokens1, tokens2);

    // But should share significant common subsequences
    let _matches = detect_duplicates_with_extension(&tokens1, 10);
    // Note: This would need cross-function detection in full implementation
}

#[test]
fn test_extension_algorithm_50_tokens() {
    // Test that we detect exactly 50-token duplicates (minimum window)
    let code = r#"
        function test() {
            let a = 1; let b = 2; let c = 3; let d = 4;
            let e = 5; let f = 6; let g = 7; let h = 8;
            return a + b + c + d + e + f + g + h;
        }

        function duplicate() {
            let a = 1; let b = 2; let c = 3; let d = 4;
            let e = 5; let f = 6; let g = 7; let h = 8;
            return a + b + c + d + e + f + g + h;
        }
    "#;

    let tokens = normalize(code);
    let matches = detect_duplicates_with_extension(&tokens, 20);

    // Should find at least one match
    assert!(!matches.is_empty());

    // All matches should be >= 20 tokens (minimum window)
    for m in &matches {
        assert!(
            m.length >= 20,
            "Match length {} is less than minimum window 20",
            m.length
        );
    }
}

#[test]
fn test_extension_algorithm_large_duplicate() {
    // Test that we detect large duplicates as single matches
    let large_code = r#"
        function processUserData(user) {
            if (!user || !user.id) throw new Error("Invalid");
            const firstName = user.firstName.trim().toLowerCase();
            const lastName = user.lastName.trim().toLowerCase();
            const birthDate = new Date(user.birthDate);
            const today = new Date();
            let age = today.getFullYear() - birthDate.getFullYear();
            const monthDiff = today.getMonth() - birthDate.getMonth();
            if (monthDiff < 0) age--;
            const profile = {
                id: user.id,
                fullName: firstName + " " + lastName,
                age: age,
                email: user.email.toLowerCase()
            };
            if (!profile.email.includes("@")) throw new Error("Invalid email");
            if (age < 18) profile.requiresParentalConsent = true;
            return profile;
        }

        function handleUserRegistration(userData) {
            if (!userData || !userData.id) throw new Error("Invalid");
            const firstName = userData.firstName.trim().toLowerCase();
            const lastName = userData.lastName.trim().toLowerCase();
            const birthDate = new Date(userData.birthDate);
            const today = new Date();
            let age = today.getFullYear() - birthDate.getFullYear();
            const monthDiff = today.getMonth() - birthDate.getMonth();
            if (monthDiff < 0) age--;
            const profile = {
                id: userData.id,
                fullName: firstName + " " + lastName,
                age: age,
                email: userData.email.toLowerCase()
            };
            if (!profile.email.includes("@")) throw new Error("Invalid email");
            if (age < 18) profile.requiresParentalConsent = true;
            return profile;
        }
    "#;

    let tokens = normalize(large_code);
    let matches = detect_duplicates_with_extension(&tokens, 50);

    // Should find one large match (not fragmented)
    assert!(!matches.is_empty());

    // The match should be significantly longer than the minimum window
    let longest_match = matches.iter().map(|m| m.length).max().unwrap();
    assert!(
        longest_match > 100,
        "Expected large duplicate >100 tokens, got {}",
        longest_match
    );
}

#[test]
fn test_no_false_positives_different_code() {
    // Ensure we don't detect duplicates in completely different code
    let code1 = r#"
        function add(a, b) {
            return a + b;
        }
    "#;

    let code2 = r#"
        function fetchUser(id) {
            return database.query("SELECT * FROM users WHERE id = ?", id);
        }
    "#;

    let tokens1 = normalize(code1);
    let tokens2 = normalize(code2);

    // These should be completely different
    assert_ne!(tokens1, tokens2);
}

#[test]
fn test_scanner_integration_with_test_files() {
    // Test the full scanner on test files if they exist
    let test_path = PathBuf::from("test_duplicates");

    if test_path.exists() {
        let scanner = Scanner::new().unwrap();
        let result = scanner.scan(vec![test_path]);

        assert!(result.is_ok());
        let report = result.unwrap();

        // Should find some duplicates in test files
        assert!(
            !report.duplicates.is_empty(),
            "Expected to find duplicates in test_duplicates/"
        );

        // All duplicates should have length >= min_block_size
        for dup in &report.duplicates {
            assert!(
                dup.length >= 50,
                "Duplicate length {} less than minimum 50",
                dup.length
            );
        }
    }
}

#[test]
fn test_extension_stops_at_divergence() {
    // Test that extension correctly stops when code diverges
    let code = r#"
        function func1() {
            let a = 1;
            let b = 2;
            let c = 3;
            return a + b + c;
        }

        function func2() {
            let a = 1;
            let b = 2;
            let c = 3;
            let d = 4;  // Extra line - divergence point
            let e = 5;
            return a + b + c + d + e;
        }
    "#;

    let tokens = normalize(code);
    let matches = detect_duplicates_with_extension(&tokens, 10);

    if !matches.is_empty() {
        // If a match is found, it should not include the divergent section
        for m in &matches {
            // The match should be shorter than the full function
            // (it should stop at the divergence point)
            assert!(
                m.length < 50,
                "Match should stop at divergence, but got length {}",
                m.length
            );
        }
    }
}

#[test]
fn test_hash_collision_handling() {
    // Ensure verify_match catches hash collisions
    // (This is a property test - in practice hash collisions are rare)
    let code = r#"
        function test1() { return 42; }
        function test2() { return 42; }
        function test3() { return 43; }
    "#;

    let tokens = normalize(code);
    let matches = detect_duplicates_with_extension(&tokens, 5);

    // Should only match truly identical sequences
    for m in &matches {
        let source = &tokens[m.source_start..m.source_start + m.length];
        let target = &tokens[m.target_start..m.target_start + m.length];
        assert_eq!(
            source, target,
            "Match verification failed - not truly identical"
        );
    }
}
