// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Tests for PatternRules and RulesMap
//!
//! This module contains comprehensive tests for the rule matching logic
//! used in bucket notifications, specifically testing the flow from
//! configuration to event matching.

use super::*;
use rustfs_s3_types::EventName;
use rustfs_targets::arn::TargetID;

#[cfg(test)]
mod pattern_rules_tests {
    use super::*;

    /// Test basic pattern rules matching
    #[test]
    fn test_pattern_rules_basic_matching() {
        let mut rules = PatternRules::new();
        let target_id = TargetID::new("test-id".to_string(), "webhook".to_string());

        // Add a rule for *.csv files
        rules.add("*.csv".to_string(), target_id.clone());

        // Test matching
        assert!(rules.match_simple("test.csv"));
        assert!(rules.match_simple("uploads/test.csv"));
        assert!(!rules.match_simple("test.txt"));

        // Test match_targets
        let targets = rules.match_targets("test.csv");
        assert!(targets.contains(&target_id));

        let targets = rules.match_targets("test.txt");
        assert!(!targets.contains(&target_id));
    }

    /// Test multiple patterns with different targets
    #[test]
    fn test_multiple_patterns() {
        let mut rules = PatternRules::new();
        let csv_target = TargetID::new("csv-target".to_string(), "webhook".to_string());
        let jpg_target = TargetID::new("jpg-target".to_string(), "webhook".to_string());

        rules.add("*.csv".to_string(), csv_target.clone());
        rules.add("*.jpg".to_string(), jpg_target.clone());

        // CSV files match csv_target
        let targets = rules.match_targets("test.csv");
        assert!(targets.contains(&csv_target));
        assert!(!targets.contains(&jpg_target));

        // JPG files match jpg_target
        let targets = rules.match_targets("test.jpg");
        assert!(!targets.contains(&csv_target));
        assert!(targets.contains(&jpg_target));

        // Both patterns match for nested paths
        let targets = rules.match_targets("uploads/test.csv");
        assert!(targets.contains(&csv_target));

        let targets = rules.match_targets("images/photo.jpg");
        assert!(targets.contains(&jpg_target));
    }

    /// Test prefix patterns
    #[test]
    fn test_prefix_patterns() {
        let mut rules = PatternRules::new();
        let target_id = TargetID::new("prefix-target".to_string(), "webhook".to_string());

        rules.add("uploads/*".to_string(), target_id.clone());
        rules.add("images/*".to_string(), target_id);

        assert!(rules.match_simple("uploads/test.csv"));
        assert!(rules.match_simple("uploads/subdir/test.csv"));
        assert!(rules.match_simple("images/photo.jpg"));
        assert!(!rules.match_simple("documents/test.txt"));
    }

    /// Test prefix + suffix pattern (bug scenario)
    #[test]
    fn test_prefix_suffix_pattern_bug_scenario() {
        let mut rules = PatternRules::new();
        let target_id = TargetID::new("webhook-target".to_string(), "webhook".to_string());

        // This is the pattern generated from prefix="uploads/", suffix=".csv"
        rules.add("uploads/*.csv".to_string(), target_id.clone());

        // These should match
        assert!(rules.match_simple("uploads/test.csv"));
        assert!(rules.match_simple("uploads/file1.csv"));
        assert!(rules.match_simple("uploads/nested/path/file.csv"));

        // These should not match
        assert!(!rules.match_simple("uploads/test.txt"));
        assert!(!rules.match_simple("files/test.csv"));

        // Verify match_targets returns correct targets
        let targets = rules.match_targets("uploads/test.csv");
        assert_eq!(targets.len(), 1);
        assert!(targets.contains(&target_id));
    }

    /// Test match_all pattern
    #[test]
    fn test_match_all_pattern() {
        let mut rules = PatternRules::new();
        let target_id = TargetID::new("all-target".to_string(), "webhook".to_string());

        rules.add("*".to_string(), target_id.clone());

        assert!(rules.match_simple("anything"));
        assert!(rules.match_simple("uploads/test.csv"));
        assert!(rules.match_simple(""));

        let targets = rules.match_targets("any_key");
        assert!(targets.contains(&target_id));
    }

    /// Test empty rules
    #[test]
    fn test_empty_rules() {
        let rules = PatternRules::new();

        assert!(rules.is_empty());
        assert!(!rules.match_simple("anything"));

        let targets = rules.match_targets("test.csv");
        assert!(targets.is_empty());
    }

    /// Test union operation
    #[test]
    fn test_union() {
        let mut rules1 = PatternRules::new();
        let mut rules2 = PatternRules::new();

        let target1 = TargetID::new("target1".to_string(), "webhook".to_string());
        let target2 = TargetID::new("target2".to_string(), "webhook".to_string());

        rules1.add("*.csv".to_string(), target1.clone());
        rules2.add("*.jpg".to_string(), target2);

        let combined = rules1.union(&rules2);

        assert!(combined.match_simple("test.csv"));
        assert!(combined.match_simple("test.jpg"));
        assert!(!combined.match_simple("test.png"));

        let targets = combined.match_targets("test.csv");
        assert!(targets.contains(&target1));
    }

    /// Test difference operation
    #[test]
    fn test_difference() {
        let mut rules1 = PatternRules::new();
        let mut rules2 = PatternRules::new();

        let target1 = TargetID::new("target1".to_string(), "webhook".to_string());
        let target2 = TargetID::new("target2".to_string(), "webhook".to_string());

        // Add same target to multiple patterns in rules1
        rules1.add("*.csv".to_string(), target1.clone());
        rules1.add("*.jpg".to_string(), target1.clone());
        rules1.add("*.txt".to_string(), target1);

        // Add different target to .jpg pattern in rules2
        rules2.add("*.jpg".to_string(), target2);

        let diff = rules1.difference(&rules2);

        // After difference:
        // *.csv: target1 remains (pattern doesn't exist in rules2)
        // *.jpg: target1 remains (target1 != target2, so it's not removed)
        // *.txt: target1 remains (pattern doesn't exist in rules2)
        assert!(diff.match_simple("test.csv"));
        assert!(diff.match_simple("test.jpg"));
        assert!(diff.match_simple("test.txt"));
    }

    /// Test difference operation removing same target
    #[test]
    fn test_difference_same_target() {
        let mut rules1 = PatternRules::new();
        let mut rules2 = PatternRules::new();

        let target1 = TargetID::new("target1".to_string(), "webhook".to_string());

        // Add same target to multiple patterns in rules1
        rules1.add("*.csv".to_string(), target1.clone());
        rules1.add("*.jpg".to_string(), target1.clone());
        rules1.add("*.txt".to_string(), target1.clone());

        // Add same target to .jpg pattern in rules2
        rules2.add("*.jpg".to_string(), target1);

        let diff = rules1.difference(&rules2);

        // After difference:
        // *.csv: target1 remains (pattern doesn't exist in rules2)
        // *.jpg: target1 is removed (same target in both)
        // *.txt: target1 remains (pattern doesn't exist in rules2)
        assert!(diff.match_simple("test.csv"));
        assert!(!diff.match_simple("test.jpg"));
        assert!(diff.match_simple("test.txt"));
    }

    /// Test remove_pattern
    #[test]
    fn test_remove_pattern() {
        let mut rules = PatternRules::new();
        let target_id = TargetID::new("test-target".to_string(), "webhook".to_string());

        rules.add("*.csv".to_string(), target_id.clone());
        rules.add("*.jpg".to_string(), target_id);

        assert!(rules.match_simple("test.csv"));
        assert!(rules.match_simple("test.jpg"));

        // Remove .csv pattern
        assert!(rules.remove_pattern("*.csv"));

        assert!(!rules.match_simple("test.csv"));
        assert!(rules.match_simple("test.jpg"));

        // Remove non-existent pattern
        assert!(!rules.remove_pattern("*.png"));
    }
}

#[cfg(test)]
mod rules_map_tests {
    use super::*;

    /// Test basic rule addition and matching
    #[test]
    fn test_rules_map_basic() {
        let mut rules_map = RulesMap::new();
        let target_id = TargetID::new("test-target".to_string(), "webhook".to_string());

        // Add rule for ObjectCreatedPut with pattern *
        rules_map.add_rule_config(&[EventName::ObjectCreatedPut], "*".to_string(), target_id.clone());

        // Check has_subscriber
        assert!(rules_map.has_subscriber(&EventName::ObjectCreatedPut));
        assert!(!rules_map.has_subscriber(&EventName::ObjectRemovedDelete));

        // Check match_rules
        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "any_key");
        assert!(targets.contains(&target_id));

        // Check event with different name doesn't match
        let targets = rules_map.match_rules(EventName::ObjectRemovedDelete, "any_key");
        assert!(!targets.contains(&target_id));
    }

    /// Test compound event expansion
    #[test]
    fn test_compound_event_expansion() {
        let mut rules_map = RulesMap::new();
        let target_id = TargetID::new("test-target".to_string(), "webhook".to_string());

        // Add rule for ObjectCreatedAll (compound event)
        rules_map.add_rule_config(&[EventName::ObjectCreatedAll], "*.csv".to_string(), target_id.clone());

        // ObjectCreatedAll should be expanded to all ObjectCreated events
        assert!(rules_map.has_subscriber(&EventName::ObjectCreatedAll));
        assert!(rules_map.has_subscriber(&EventName::ObjectCreatedPut));
        assert!(rules_map.has_subscriber(&EventName::ObjectCreatedPost));
        assert!(rules_map.has_subscriber(&EventName::ObjectCreatedCopy));

        // All should match .csv files
        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "test.csv");
        assert!(targets.contains(&target_id));

        let targets = rules_map.match_rules(EventName::ObjectCreatedPost, "test.csv");
        assert!(targets.contains(&target_id));
    }

    /// Test exact bug scenario
    #[test]
    fn test_bug_scenario_exact_flow() {
        let mut rules_map = RulesMap::new();
        let target_id = TargetID::new("webhook-primary".to_string(), "webhook".to_string());

        // Configuration: Events=["s3:ObjectCreated:*"], prefix="uploads/", suffix=".csv"
        // This generates pattern "uploads/*.csv"
        rules_map.add_rule_config(&[EventName::ObjectCreatedAll], "uploads/*.csv".to_string(), target_id.clone());

        // When ObjectCreatedPut event occurs with key "uploads/test.csv"
        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "uploads/test.csv");

        // Should match
        assert!(!targets.is_empty(), "Targets should not be empty for matching event");
        assert!(targets.contains(&target_id), "Should find webhook-primary target");

        // Test non-matching key
        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "uploads/test.txt");
        assert!(targets.is_empty(), "Targets should be empty for non-matching key");

        // Test matching different suffix
        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "uploads/subdir/file.csv");
        assert!(!targets.is_empty(), "Nested CSV files should also match");
        assert!(targets.contains(&target_id));
    }

    /// Test multiple patterns for same event
    #[test]
    fn test_multiple_patterns_same_event() {
        let mut rules_map = RulesMap::new();
        let csv_target = TargetID::new("csv-target".to_string(), "webhook".to_string());
        let jpg_target = TargetID::new("jpg-target".to_string(), "webhook".to_string());

        // Add different patterns for same event
        rules_map.add_rule_config(&[EventName::ObjectCreatedPut], "*.csv".to_string(), csv_target.clone());
        rules_map.add_rule_config(&[EventName::ObjectCreatedPut], "*.jpg".to_string(), jpg_target.clone());

        // Both patterns should work
        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "test.csv");
        assert!(targets.contains(&csv_target));
        assert!(!targets.contains(&jpg_target));

        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "test.jpg");
        assert!(!targets.contains(&csv_target));
        assert!(targets.contains(&jpg_target));
    }

    /// Test prefix only filter
    #[test]
    fn test_prefix_only_filter() {
        let mut rules_map = RulesMap::new();
        let target_id = TargetID::new("test-target".to_string(), "webhook".to_string());

        // Configuration: prefix="images/", no suffix
        // Pattern should be "images/*"
        rules_map.add_rule_config(&[EventName::ObjectCreatedAll], "images/*".to_string(), target_id.clone());

        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "images/photo.jpg");
        assert!(targets.contains(&target_id));

        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "uploads/photo.jpg");
        assert!(!targets.contains(&target_id));
    }

    /// Test suffix only filter
    #[test]
    fn test_suffix_only_filter() {
        let mut rules_map = RulesMap::new();
        let target_id = TargetID::new("test-target".to_string(), "webhook".to_string());

        // Configuration: no prefix, suffix=".pdf"
        // Pattern should be "*.pdf"
        rules_map.add_rule_config(&[EventName::ObjectCreatedAll], "*.pdf".to_string(), target_id.clone());

        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "document.pdf");
        assert!(targets.contains(&target_id));

        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "document.txt");
        assert!(!targets.contains(&target_id));
    }

    /// Test empty pattern (no filter)
    #[test]
    fn test_no_filter() {
        let mut rules_map = RulesMap::new();
        let target_id = TargetID::new("test-target".to_string(), "webhook".to_string());

        // Configuration: no prefix, no suffix
        // Pattern should default to "*" (match all)
        rules_map.add_rule_config(&[EventName::ObjectCreatedAll], "".to_string(), target_id.clone());

        // All keys should match
        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "anything.csv");
        assert!(targets.contains(&target_id));

        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "uploads/test.txt");
        assert!(targets.contains(&target_id));
    }

    /// Test different event types
    #[test]
    fn test_different_event_types() {
        let mut rules_map = RulesMap::new();
        let put_target = TargetID::new("put-target".to_string(), "webhook".to_string());
        let delete_target = TargetID::new("delete-target".to_string(), "webhook".to_string());

        // Add rules for different event types
        rules_map.add_rule_config(&[EventName::ObjectCreatedPut], "*.csv".to_string(), put_target.clone());
        rules_map.add_rule_config(&[EventName::ObjectRemovedDelete], "*.csv".to_string(), delete_target.clone());

        // Put event should match put_target
        let targets = rules_map.match_rules(EventName::ObjectCreatedPut, "test.csv");
        assert!(targets.contains(&put_target));
        assert!(!targets.contains(&delete_target));

        // Delete event should match delete_target
        let targets = rules_map.match_rules(EventName::ObjectRemovedDelete, "test.csv");
        assert!(!targets.contains(&put_target));
        assert!(targets.contains(&delete_target));
    }

    /// Test remove_map
    #[test]
    fn test_remove_map() {
        let mut rules_map1 = RulesMap::new();
        let mut rules_map2 = RulesMap::new();

        let target1 = TargetID::new("target1".to_string(), "webhook".to_string());
        let target2 = TargetID::new("target2".to_string(), "webhook".to_string());

        rules_map1.add_rule_config(&[EventName::ObjectCreatedPut], "*.csv".to_string(), target1.clone());
        rules_map1.add_rule_config(&[EventName::ObjectCreatedPost], "*.jpg".to_string(), target1.clone());

        rules_map2.add_rule_config(&[EventName::ObjectCreatedPut], "*.csv".to_string(), target2.clone());

        // Remove rules_map2 from rules_map1
        rules_map1.remove_map(&rules_map2);

        // ObjectCreatedPut rule should still exist with target1 (different from target2)
        let targets = rules_map1.match_rules(EventName::ObjectCreatedPut, "test.csv");
        assert!(targets.contains(&target1));
        assert!(!targets.contains(&target2));

        // ObjectCreatedPost rule should still exist
        let targets = rules_map1.match_rules(EventName::ObjectCreatedPost, "test.jpg");
        assert!(targets.contains(&target1));
    }

    /// Test remove_map removing same target
    #[test]
    fn test_remove_map_same_target() {
        let mut rules_map1 = RulesMap::new();
        let mut rules_map2 = RulesMap::new();

        let target1 = TargetID::new("target1".to_string(), "webhook".to_string());

        rules_map1.add_rule_config(&[EventName::ObjectCreatedPut], "*.csv".to_string(), target1.clone());
        rules_map1.add_rule_config(&[EventName::ObjectCreatedPost], "*.jpg".to_string(), target1.clone());

        rules_map2.add_rule_config(&[EventName::ObjectCreatedPut], "*.csv".to_string(), target1.clone());

        // Remove rules_map2 from rules_map1
        rules_map1.remove_map(&rules_map2);

        // ObjectCreatedPut rule should be removed (same target and pattern)
        let targets = rules_map1.match_rules(EventName::ObjectCreatedPut, "test.csv");
        assert!(targets.is_empty());

        // ObjectCreatedPost rule should still exist
        let targets = rules_map1.match_rules(EventName::ObjectCreatedPost, "test.jpg");
        assert!(targets.contains(&target1));
    }

    /// Test contains_target_id
    #[test]
    fn test_contains_target_id() {
        let mut rules_map = RulesMap::new();
        let target_id = TargetID::new("test-target".to_string(), "webhook".to_string());

        assert!(!rules_map.contains_target_id(&target_id));

        rules_map.add_rule_config(&[EventName::ObjectCreatedPut], "*.csv".to_string(), target_id.clone());

        assert!(rules_map.contains_target_id(&target_id));
    }
}
