//! Testing utilities for plugin development.
//!
//! This module provides two complementary approaches for testing plugins:
//!
//! - [`PluginTestRunner`] - A test runner with assertion methods and fixture-based testing
//! - [`TestCase`] - A builder for inline, declarative test assertions
//!
//! # Quick Example
//!
//! ```
//! use nginx_lint_plugin::prelude::*;
//! use nginx_lint_plugin::testing::{PluginTestRunner, TestCase};
//!
//! // Define a simple plugin for demonstration
//! # #[derive(Default)]
//! # struct MyPlugin;
//! # impl Plugin for MyPlugin {
//! #     fn spec(&self) -> PluginSpec {
//! #         PluginSpec::new("my-rule", "test", "Test rule").with_severity("warning")
//! #     }
//! #     fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
//! #         let err = self.spec().error_builder();
//! #         config.all_directives()
//! #             .filter(|d| d.is("bad_directive"))
//! #             .map(|d| err.warning_at("bad", d))
//! #             .collect()
//! #     }
//! # }
//!
//! // Use PluginTestRunner for quick assertions
//! let runner = PluginTestRunner::new(MyPlugin);
//! runner.assert_has_errors("http {\n    bad_directive on;\n}");
//! runner.assert_no_errors("http {\n    good_directive on;\n}");
//!
//! // Use TestCase for declarative, detailed assertions
//! TestCase::new("http {\n    bad_directive on;\n}")
//!     .expect_error_count(1)
//!     .expect_error_on_line(2)
//!     .run(&MyPlugin);
//! ```
//!
//! # Fixture Directory Structure
//!
//! ```text
//! tests/fixtures/
//! └── 001_basic/
//!     ├── error/nginx.conf      # Config that should trigger errors
//!     └── expected/nginx.conf   # Config after applying fixes (no errors expected)
//! ```

use super::types::{Config, Fix, LintError, Plugin, PluginSpec};
use std::path::{Path, PathBuf};

/// Macro to get the fixtures directory path relative to the plugin's Cargo.toml
///
/// Usage in plugin tests:
/// ```ignore
/// runner.test_fixtures(nginx_lint_plugin::fixtures_dir!());
/// ```
#[macro_export]
macro_rules! fixtures_dir {
    () => {
        concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures")
    };
}

/// Test runner for plugins.
///
/// Provides assertion methods for testing plugin behavior against nginx config strings
/// and fixture directories.
///
/// # Example
///
/// ```
/// use nginx_lint_plugin::prelude::*;
/// use nginx_lint_plugin::testing::PluginTestRunner;
///
/// # #[derive(Default)]
/// # struct MyPlugin;
/// # impl Plugin for MyPlugin {
/// #     fn spec(&self) -> PluginSpec {
/// #         PluginSpec::new("my-rule", "test", "Test rule")
/// #     }
/// #     fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
/// #         let err = self.spec().error_builder();
/// #         config.all_directives()
/// #             .filter(|d| d.is("bad"))
/// #             .map(|d| err.warning_at("bad", d))
/// #             .collect()
/// #     }
/// # }
/// let runner = PluginTestRunner::new(MyPlugin);
///
/// // Test a config string
/// runner.assert_has_errors("http {\n    bad on;\n}");
/// runner.assert_no_errors("http {\n    good on;\n}");
/// runner.assert_errors("http {\n    bad on;\n    bad on;\n}", 2);
/// runner.assert_error_on_line("http {\n    bad on;\n}", 2);
/// ```
pub struct PluginTestRunner<P: Plugin> {
    plugin: P,
}

impl<P: Plugin> PluginTestRunner<P> {
    /// Create a new test runner for a plugin
    pub fn new(plugin: P) -> Self {
        Self { plugin }
    }

    /// Get plugin spec
    pub fn spec(&self) -> PluginSpec {
        self.plugin.spec()
    }

    /// Run the plugin check on a config string
    pub fn check_string(&self, content: &str) -> Result<Vec<LintError>, String> {
        let config: Config = nginx_lint_common::parse_string(content)
            .map_err(|e| format!("Failed to parse config: {}", e))?;
        Ok(self.plugin.check(&config, "test.conf"))
    }

    /// Run the plugin check on a file
    pub fn check_file(&self, path: &Path) -> Result<Vec<LintError>, String> {
        let content =
            std::fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
        let config: Config = nginx_lint_common::parse_string(&content)
            .map_err(|e| format!("Failed to parse config: {}", e))?;
        Ok(self.plugin.check(&config, path.to_string_lossy().as_ref()))
    }

    /// Test all fixtures in a directory
    pub fn test_fixtures(&self, fixtures_dir: &str) {
        let fixtures_path = PathBuf::from(fixtures_dir);
        if !fixtures_path.exists() {
            panic!("Fixtures directory not found: {}", fixtures_dir);
        }

        let plugin_spec = self.plugin.spec();
        let rule_name = &plugin_spec.name;

        let entries = std::fs::read_dir(&fixtures_path)
            .unwrap_or_else(|e| panic!("Failed to read fixtures directory: {}", e));

        let mut tested_count = 0;

        for entry in entries {
            let entry = entry.expect("Failed to read directory entry");
            let case_path = entry.path();

            if !case_path.is_dir() {
                continue;
            }

            let case_name = case_path.file_name().unwrap().to_string_lossy();
            self.test_case(&case_path, rule_name, &case_name);
            tested_count += 1;
        }

        if tested_count == 0 {
            panic!("No test cases found in {}", fixtures_dir);
        }
    }

    /// Test a single fixture case
    fn test_case(&self, case_path: &Path, rule_name: &str, case_name: &str) {
        let error_path = case_path.join("error").join("nginx.conf");
        let expected_path = case_path.join("expected").join("nginx.conf");

        if error_path.exists() {
            let errors = self
                .check_file(&error_path)
                .unwrap_or_else(|e| panic!("Failed to check error fixture {}: {}", case_name, e));

            let rule_errors: Vec<_> = errors.iter().filter(|e| e.rule == rule_name).collect();

            assert!(
                !rule_errors.is_empty(),
                "Expected {} errors in {}/error/nginx.conf, got none",
                rule_name,
                case_name
            );
        }

        if expected_path.exists() {
            let errors = self.check_file(&expected_path).unwrap_or_else(|e| {
                panic!("Failed to check expected fixture {}: {}", case_name, e)
            });

            let rule_errors: Vec<_> = errors.iter().filter(|e| e.rule == rule_name).collect();

            assert!(
                rule_errors.is_empty(),
                "Expected no {} errors in {}/expected/nginx.conf, got: {:?}",
                rule_name,
                case_name,
                rule_errors
            );
        }
    }

    /// Assert that a config string produces specific errors
    pub fn assert_errors(&self, content: &str, expected_count: usize) {
        let errors = self.check_string(content).expect("Failed to check config");
        let plugin_spec = self.plugin.spec();
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();

        assert_eq!(
            rule_errors.len(),
            expected_count,
            "Expected {} errors from {}, got {}: {:?}",
            expected_count,
            plugin_spec.name,
            rule_errors.len(),
            rule_errors
        );
    }

    /// Assert that a config string produces no errors
    pub fn assert_no_errors(&self, content: &str) {
        self.assert_errors(content, 0);
    }

    /// Assert that a config string produces at least one error
    pub fn assert_has_errors(&self, content: &str) {
        let errors = self.check_string(content).expect("Failed to check config");
        let plugin_spec = self.plugin.spec();
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();

        assert!(
            !rule_errors.is_empty(),
            "Expected at least one error from {}, got none",
            plugin_spec.name
        );
    }

    /// Assert that a config string produces an error on a specific line
    pub fn assert_error_on_line(&self, content: &str, expected_line: usize) {
        let errors = self.check_string(content).expect("Failed to check config");
        let plugin_spec = self.plugin.spec();
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();

        let has_error_on_line = rule_errors.iter().any(|e| e.line == Some(expected_line));

        assert!(
            has_error_on_line,
            "Expected error from {} on line {}, got errors on lines: {:?}",
            plugin_spec.name,
            expected_line,
            rule_errors.iter().map(|e| e.line).collect::<Vec<_>>()
        );
    }

    /// Assert that errors contain a specific message substring
    pub fn assert_error_message_contains(&self, content: &str, expected_substring: &str) {
        let errors = self.check_string(content).expect("Failed to check config");
        let plugin_spec = self.plugin.spec();
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();

        let has_message = rule_errors
            .iter()
            .any(|e| e.message.contains(expected_substring));

        assert!(
            has_message,
            "Expected error message containing '{}' from {}, got messages: {:?}",
            expected_substring,
            plugin_spec.name,
            rule_errors.iter().map(|e| &e.message).collect::<Vec<_>>()
        );
    }

    /// Assert that errors have fixes
    pub fn assert_has_fix(&self, content: &str) {
        let errors = self.check_string(content).expect("Failed to check config");
        let plugin_spec = self.plugin.spec();
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();

        let has_fix = rule_errors.iter().any(|e| !e.fixes.is_empty());

        assert!(
            has_fix,
            "Expected at least one error with fix from {}, got errors: {:?}",
            plugin_spec.name, rule_errors
        );
    }

    /// Assert that applying fixes produces the expected output
    pub fn assert_fix_produces(&self, content: &str, expected: &str) {
        let errors = self.check_string(content).expect("Failed to check config");
        let plugin_spec = self.plugin.spec();

        let fixes: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .flat_map(|e| e.fixes.iter())
            .collect();

        assert!(
            !fixes.is_empty(),
            "Expected at least one fix from {}, got none",
            plugin_spec.name
        );

        let result = apply_fixes(content, &fixes);
        let expected_normalized = expected.trim();
        let result_normalized = result.trim();

        assert_eq!(
            result_normalized, expected_normalized,
            "Fix did not produce expected output.\nExpected:\n{}\n\nGot:\n{}",
            expected_normalized, result_normalized
        );
    }

    /// Test using bad.conf and good.conf example content
    pub fn test_examples(&self, bad_conf: &str, good_conf: &str) {
        let plugin_spec = self.plugin.spec();

        let errors = self
            .check_string(bad_conf)
            .expect("Failed to parse bad.conf");
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();
        assert!(
            !rule_errors.is_empty(),
            "bad.conf should produce at least one {} error, got none",
            plugin_spec.name
        );

        let errors = self
            .check_string(good_conf)
            .expect("Failed to parse good.conf");
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();
        assert!(
            rule_errors.is_empty(),
            "good.conf should not produce {} errors, got: {:?}",
            plugin_spec.name,
            rule_errors
        );
    }

    /// Test using bad.conf and good.conf, and verify fix converts bad to good
    pub fn test_examples_with_fix(&self, bad_conf: &str, good_conf: &str) {
        let plugin_spec = self.plugin.spec();

        let errors = self
            .check_string(bad_conf)
            .expect("Failed to parse bad.conf");
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();
        assert!(
            !rule_errors.is_empty(),
            "bad.conf should produce at least one {} error, got none",
            plugin_spec.name
        );

        let fixes: Vec<_> = rule_errors.iter().flat_map(|e| e.fixes.iter()).collect();
        assert!(
            !fixes.is_empty(),
            "bad.conf errors should have fixes, got none"
        );

        let errors = self
            .check_string(good_conf)
            .expect("Failed to parse good.conf");
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();
        assert!(
            rule_errors.is_empty(),
            "good.conf should not produce {} errors, got: {:?}",
            plugin_spec.name,
            rule_errors
        );

        let fixed = apply_fixes(bad_conf, &fixes);
        assert_eq!(
            fixed.trim(),
            good_conf.trim(),
            "Applying fixes to bad.conf should produce good.conf.\nExpected:\n{}\n\nGot:\n{}",
            good_conf.trim(),
            fixed.trim()
        );
    }
}

/// Declarative test builder for inline plugin tests.
///
/// Chain expectations and then call [`run()`](TestCase::run) to execute:
///
/// ```
/// use nginx_lint_plugin::prelude::*;
/// use nginx_lint_plugin::testing::TestCase;
///
/// # #[derive(Default)]
/// # struct MyPlugin;
/// # impl Plugin for MyPlugin {
/// #     fn spec(&self) -> PluginSpec {
/// #         PluginSpec::new("my-rule", "test", "Test rule")
/// #     }
/// #     fn check(&self, config: &Config, _path: &str) -> Vec<LintError> {
/// #         let err = self.spec().error_builder();
/// #         config.all_directives()
/// #             .filter(|d| d.is("autoindex") && d.first_arg_is("on"))
/// #             .map(|d| err.warning_at("autoindex should be off", d)
/// #                 .with_fix(d.replace_with("autoindex off;")))
/// #             .collect()
/// #     }
/// # }
/// TestCase::new("http {\n    autoindex on;\n}")
///     .expect_error_count(1)
///     .expect_error_on_line(2)
///     .expect_message_contains("autoindex")
///     .expect_has_fix()
///     .expect_fix_produces("http {\n    autoindex off;\n}")
///     .run(&MyPlugin);
/// ```
///
/// # Available Expectations
///
/// | Method | Description |
/// |--------|-------------|
/// | [`expect_error_count(n)`](TestCase::expect_error_count) | Exact error count |
/// | [`expect_no_errors()`](TestCase::expect_no_errors) | No errors |
/// | [`expect_error_on_line(n)`](TestCase::expect_error_on_line) | Error on specific line |
/// | [`expect_message_contains(s)`](TestCase::expect_message_contains) | Error message substring |
/// | [`expect_has_fix()`](TestCase::expect_has_fix) | At least one error has a fix |
/// | [`expect_fix_on_line(n)`](TestCase::expect_fix_on_line) | Fix targets specific line |
/// | [`expect_fix_produces(s)`](TestCase::expect_fix_produces) | Verify fix output |
pub struct TestCase {
    content: String,
    expected_error_count: Option<usize>,
    expected_lines: Vec<usize>,
    expected_message_contains: Vec<String>,
    expect_has_fix: bool,
    expected_fix_output: Option<String>,
    expected_fix_on_lines: Vec<usize>,
}

impl TestCase {
    /// Create a new test case with the given config content
    pub fn new(content: impl Into<String>) -> Self {
        Self {
            content: content.into(),
            expected_error_count: None,
            expected_lines: Vec::new(),
            expected_message_contains: Vec::new(),
            expect_has_fix: false,
            expected_fix_output: None,
            expected_fix_on_lines: Vec::new(),
        }
    }

    /// Expect a specific number of errors
    pub fn expect_error_count(mut self, count: usize) -> Self {
        self.expected_error_count = Some(count);
        self
    }

    /// Expect no errors
    pub fn expect_no_errors(self) -> Self {
        self.expect_error_count(0)
    }

    /// Expect at least one error on the given line
    pub fn expect_error_on_line(mut self, line: usize) -> Self {
        self.expected_lines.push(line);
        self
    }

    /// Expect error messages to contain the given substring
    pub fn expect_message_contains(mut self, substring: impl Into<String>) -> Self {
        self.expected_message_contains.push(substring.into());
        self
    }

    /// Expect at least one error to have a fix
    pub fn expect_has_fix(mut self) -> Self {
        self.expect_has_fix = true;
        self
    }

    /// Expect a fix on a specific line
    pub fn expect_fix_on_line(mut self, line: usize) -> Self {
        self.expected_fix_on_lines.push(line);
        self.expect_has_fix = true;
        self
    }

    /// Expect that applying all fixes produces the given output
    pub fn expect_fix_produces(mut self, expected: impl Into<String>) -> Self {
        self.expected_fix_output = Some(expected.into());
        self.expect_has_fix = true;
        self
    }

    /// Run the test case with the given plugin
    pub fn run<P: Plugin>(self, plugin: &P) {
        let config: Config = nginx_lint_common::parse_string(&self.content)
            .unwrap_or_else(|e| panic!("Failed to parse test config: {}", e));

        let errors = plugin.check(&config, "test.conf");
        let plugin_spec = plugin.spec();
        let rule_errors: Vec<_> = errors
            .iter()
            .filter(|e| e.rule == plugin_spec.name)
            .collect();

        if let Some(expected_count) = self.expected_error_count {
            assert_eq!(
                rule_errors.len(),
                expected_count,
                "Expected {} errors, got {}: {:?}",
                expected_count,
                rule_errors.len(),
                rule_errors
            );
        }

        for expected_line in &self.expected_lines {
            let has_error = rule_errors.iter().any(|e| e.line == Some(*expected_line));
            assert!(
                has_error,
                "Expected error on line {}, got errors on lines: {:?}",
                expected_line,
                rule_errors.iter().map(|e| e.line).collect::<Vec<_>>()
            );
        }

        for expected_msg in &self.expected_message_contains {
            let has_message = rule_errors.iter().any(|e| e.message.contains(expected_msg));
            assert!(
                has_message,
                "Expected error message containing '{}', got: {:?}",
                expected_msg,
                rule_errors.iter().map(|e| &e.message).collect::<Vec<_>>()
            );
        }

        if self.expect_has_fix {
            let has_fix = rule_errors.iter().any(|e| !e.fixes.is_empty());
            assert!(
                has_fix,
                "Expected at least one error with fix, got errors: {:?}",
                rule_errors
            );
        }

        for expected_line in &self.expected_fix_on_lines {
            let has_fix_on_line = rule_errors.iter().flat_map(|e| e.fixes.iter()).any(|f| {
                if f.is_range_based() {
                    fix_covers_line(&self.content, f, *expected_line)
                } else {
                    f.line == *expected_line
                }
            });
            assert!(
                has_fix_on_line,
                "Expected fix on line {}, got fixes on lines: {:?}",
                expected_line,
                rule_errors
                    .iter()
                    .flat_map(|e| e.fixes.iter().map(|f| {
                        if f.is_range_based() {
                            let start = f.start_offset.unwrap_or(0);
                            let end = f.end_offset.unwrap_or(start);
                            let start_line = offset_to_line(&self.content, start);
                            let end_line = offset_to_line(&self.content, end);
                            if start_line == end_line {
                                start_line
                            } else {
                                // Show the primary target line (after any leading newline)
                                let first_byte = self.content.as_bytes().get(start);
                                if first_byte == Some(&b'\n') {
                                    start_line + 1
                                } else {
                                    start_line
                                }
                            }
                        } else {
                            f.line
                        }
                    }))
                    .collect::<Vec<_>>()
            );
        }

        if let Some(expected_output) = &self.expected_fix_output {
            let fixes: Vec<_> = rule_errors.iter().flat_map(|e| e.fixes.iter()).collect();

            assert!(
                !fixes.is_empty(),
                "Expected at least one fix to check output, got none"
            );

            let result = apply_fixes(&self.content, &fixes);
            let expected_normalized = expected_output.trim();
            let result_normalized = result.trim();

            assert_eq!(
                result_normalized, expected_normalized,
                "Fix did not produce expected output.\nExpected:\n{}\n\nGot:\n{}",
                expected_normalized, result_normalized
            );
        }
    }
}

/// Convert a byte offset to a 1-based line number
fn offset_to_line(content: &str, offset: usize) -> usize {
    let offset = offset.min(content.len());
    content[..offset].chars().filter(|&c| c == '\n').count() + 1
}

/// Check if a range-based fix covers (affects) the given line.
///
/// A fix that deletes a line often includes the preceding `\n`, so checking
/// only `start_offset` would point to the previous line. This function checks
/// whether the fix's byte range [start, end) spans any byte on the target line.
fn fix_covers_line(content: &str, fix: &Fix, line: usize) -> bool {
    let start = fix.start_offset.unwrap_or(0);
    let end = fix.end_offset.unwrap_or(start);
    let start_line = offset_to_line(content, start);
    // end is exclusive, so subtract 1 to get the line of the last affected byte
    let end_line = offset_to_line(content, end.max(1) - if end > start { 1 } else { 0 });
    line >= start_line && line <= end_line
}

/// Apply fixes to content and return the result.
///
/// Converts plugin `Fix` to common `Fix` and delegates to
/// `nginx_lint_common::apply_fixes_to_content` for normalization, overlap detection, and ordering.
fn apply_fixes(content: &str, fixes: &[&Fix]) -> String {
    let common_fixes: Vec<nginx_lint_common::Fix> = fixes
        .iter()
        .map(|f| nginx_lint_common::Fix {
            line: f.line,
            old_text: f.old_text.clone(),
            new_text: f.new_text.clone(),
            delete_line: f.delete_line,
            insert_after: f.insert_after,
            start_offset: f.start_offset,
            end_offset: f.end_offset,
        })
        .collect();
    let common_refs: Vec<&nginx_lint_common::Fix> = common_fixes.iter().collect();
    let (result, _) = nginx_lint_common::apply_fixes_to_content(content, &common_refs);
    result
}
