//! HTML Coverage Report Formatter (Feature 12)
//!
//! Interactive HTML coverage reports with source code highlighting.

use crate::coverage::CoverageReport;
use crate::result::ProbarResult;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;

/// Block coverage data: (line, hit_count, function_name)
type BlockCoverageData = Vec<(u32, u64, Option<String>)>;

/// Files grouped by path
type FileMap = BTreeMap<String, BlockCoverageData>;

/// Color theme for HTML report
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Theme {
    /// Light theme (default)
    #[default]
    Light,
    /// Dark theme
    Dark,
    /// High contrast theme
    HighContrast,
}

/// Configuration for HTML coverage report
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HtmlReportConfig {
    /// Report title
    pub title: String,
    /// Highlight uncovered lines
    pub highlight_uncovered: bool,
    /// Include branch coverage information
    pub include_branch_coverage: bool,
    /// Color theme
    pub theme: Theme,
    /// Show line numbers
    pub show_line_numbers: bool,
}

impl Default for HtmlReportConfig {
    fn default() -> Self {
        Self {
            title: "Coverage Report".to_string(),
            highlight_uncovered: true,
            include_branch_coverage: false,
            theme: Theme::Light,
            show_line_numbers: true,
        }
    }
}

impl HtmlReportConfig {
    /// Create new config with default settings
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the report title
    #[must_use]
    pub fn with_title(mut self, title: impl Into<String>) -> Self {
        self.title = title.into();
        self
    }

    /// Set highlight uncovered option
    #[must_use]
    pub fn with_highlight_uncovered(mut self, highlight: bool) -> Self {
        self.highlight_uncovered = highlight;
        self
    }

    /// Set branch coverage option
    #[must_use]
    pub fn with_branch_coverage(mut self, include: bool) -> Self {
        self.include_branch_coverage = include;
        self
    }

    /// Set the theme
    #[must_use]
    pub fn with_theme(mut self, theme: Theme) -> Self {
        self.theme = theme;
        self
    }

    /// Set show line numbers option
    #[must_use]
    pub fn with_line_numbers(mut self, show: bool) -> Self {
        self.show_line_numbers = show;
        self
    }
}

/// HTML format report generator
#[derive(Debug)]
pub struct HtmlFormatter<'a> {
    report: &'a CoverageReport,
    config: HtmlReportConfig,
}

impl<'a> HtmlFormatter<'a> {
    /// Create a new HTML formatter with default config
    #[must_use]
    pub fn new(report: &'a CoverageReport) -> Self {
        Self {
            report,
            config: HtmlReportConfig::default(),
        }
    }

    /// Create with custom configuration
    #[must_use]
    pub fn with_config(report: &'a CoverageReport, config: HtmlReportConfig) -> Self {
        Self { report, config }
    }

    /// Generate the HTML report as a string
    #[must_use]
    pub fn generate(&self) -> String {
        let summary = self.report.summary();
        let files = self.group_by_file();

        let css = Self::generate_css();
        let summary_html = Self::generate_summary_section(&summary);
        let files_html = Self::generate_files_section(&files);

        format!(
            r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{title}</title>
    <style>{css}</style>
</head>
<body class="{theme_class}">
    <header>
        <h1>{title}</h1>
        <p>Generated by Probar</p>
    </header>
    <main>
        {summary_html}
        {files_html}
    </main>
    <footer>
        <p>Probar Coverage Report</p>
    </footer>
</body>
</html>"#,
            title = self.config.title,
            css = css,
            theme_class = self.theme_class(),
            summary_html = summary_html,
            files_html = files_html,
        )
    }

    /// Save the HTML report to a directory
    ///
    /// # Errors
    ///
    /// Returns error if file write fails
    pub fn save(&self, output_dir: &Path) -> ProbarResult<()> {
        std::fs::create_dir_all(output_dir)?;

        let index_path = output_dir.join("index.html");
        let content = self.generate();
        std::fs::write(index_path, content)?;

        Ok(())
    }

    /// Get CSS class for current theme
    fn theme_class(&self) -> &'static str {
        match self.config.theme {
            Theme::Light => "theme-light",
            Theme::Dark => "theme-dark",
            Theme::HighContrast => "theme-high-contrast",
        }
    }

    /// Generate CSS styles
    fn generate_css() -> &'static str {
        r#"
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; padding: 20px; }
        .theme-light { background: #fff; color: #333; }
        .theme-dark { background: #1e1e1e; color: #d4d4d4; }
        .theme-high-contrast { background: #000; color: #fff; }
        header { margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #ccc; }
        h1 { font-size: 24px; }
        h2 { font-size: 18px; margin: 20px 0 10px; }
        .summary { display: flex; gap: 20px; margin: 20px 0; flex-wrap: wrap; }
        .summary-card { padding: 15px 20px; border-radius: 8px; min-width: 150px; }
        .theme-light .summary-card { background: #f5f5f5; }
        .theme-dark .summary-card { background: #2d2d2d; }
        .summary-card h3 { font-size: 14px; color: #666; }
        .theme-dark .summary-card h3 { color: #999; }
        .summary-card .value { font-size: 28px; font-weight: bold; }
        .coverage-bar { height: 20px; background: #e0e0e0; border-radius: 10px; overflow: hidden; margin: 10px 0; }
        .coverage-fill { height: 100%; background: linear-gradient(90deg, #4caf50, #8bc34a); }
        .file-list { margin: 20px 0; }
        .file-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; }
        .theme-dark .file-item { border-color: #444; }
        .file-name { font-family: monospace; }
        .file-coverage { font-weight: bold; }
        .covered { color: #4caf50; }
        .uncovered { color: #f44336; }
        footer { margin-top: 40px; padding-top: 10px; border-top: 1px solid #ccc; color: #666; font-size: 12px; }
        "#
    }

    /// Generate summary section HTML
    fn generate_summary_section(summary: &crate::coverage::CoverageSummary) -> String {
        let coverage_color = if summary.coverage_percent >= 80.0 {
            "covered"
        } else if summary.coverage_percent >= 50.0 {
            ""
        } else {
            "uncovered"
        };

        format!(
            r#"<section class="summary">
    <div class="summary-card">
        <h3>Total Blocks</h3>
        <div class="value">{total}</div>
    </div>
    <div class="summary-card">
        <h3>Covered</h3>
        <div class="value covered">{covered}</div>
    </div>
    <div class="summary-card">
        <h3>Coverage</h3>
        <div class="value {color}">{percent:.1}%</div>
    </div>
</section>
<div class="coverage-bar">
    <div class="coverage-fill" style="width: {percent}%"></div>
</div>"#,
            total = summary.total_blocks,
            covered = summary.covered_blocks,
            percent = summary.coverage_percent,
            color = coverage_color,
        )
    }

    /// Generate files section HTML
    fn generate_files_section(files: &FileMap) -> String {
        use std::fmt::Write;
        let mut html = String::from("<section class=\"file-list\"><h2>Files</h2>");

        for (file, blocks) in files {
            let covered = blocks.iter().filter(|(_, count, _)| *count > 0).count();
            let total = blocks.len();
            let percent = if total > 0 {
                (covered as f64 / total as f64) * 100.0
            } else {
                100.0
            };

            let color = if percent >= 80.0 {
                "covered"
            } else {
                "uncovered"
            };

            let _ = write!(
                html,
                r#"<div class="file-item">
    <span class="file-name">{file}</span>
    <span class="file-coverage {color}">{covered}/{total} ({percent:.1}%)</span>
</div>"#,
            );
        }

        html.push_str("</section>");
        html
    }

    /// Group coverage data by source file
    fn group_by_file(&self) -> FileMap {
        let mut files: FileMap = BTreeMap::new();

        for block in self.report.block_coverages() {
            let file = block.source_location.as_ref().map_or_else(
                || "unknown".to_string(),
                |loc| loc.split(':').next().unwrap_or("unknown").to_string(),
            );

            let line = block.source_location.as_ref().map_or(0, |loc| {
                loc.split(':')
                    .nth(1)
                    .and_then(|l| l.parse().ok())
                    .unwrap_or(0)
            });

            files
                .entry(file)
                .or_default()
                .push((line, block.hit_count, block.function_name));
        }

        files
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;
    use crate::coverage::BlockId;

    fn create_test_report() -> CoverageReport {
        let mut report = CoverageReport::new(5);
        report.set_session_name("test_session");

        report.record_hits(BlockId::new(0), 10);
        report.record_hits(BlockId::new(1), 5);
        report.record_hits(BlockId::new(2), 0);
        report.record_hits(BlockId::new(3), 3);
        report.record_hits(BlockId::new(4), 0);

        report.set_source_location(BlockId::new(0), "src/game.rs:10");
        report.set_source_location(BlockId::new(1), "src/game.rs:15");
        report.set_source_location(BlockId::new(2), "src/game.rs:20");
        report.set_source_location(BlockId::new(3), "src/player.rs:5");
        report.set_source_location(BlockId::new(4), "src/player.rs:10");

        report
    }

    mod config_tests {
        use super::*;

        #[test]
        fn test_default_config() {
            let config = HtmlReportConfig::default();
            assert_eq!(config.title, "Coverage Report");
            assert!(config.highlight_uncovered);
            assert!(!config.include_branch_coverage);
            assert_eq!(config.theme, Theme::Light);
            assert!(config.show_line_numbers);
        }

        #[test]
        fn test_config_with_title() {
            let config = HtmlReportConfig::new().with_title("My Report");
            assert_eq!(config.title, "My Report");
        }

        #[test]
        fn test_config_with_theme() {
            let config = HtmlReportConfig::new().with_theme(Theme::Dark);
            assert_eq!(config.theme, Theme::Dark);
        }

        #[test]
        fn test_config_chained_builders() {
            let config = HtmlReportConfig::new()
                .with_title("Test")
                .with_theme(Theme::HighContrast)
                .with_highlight_uncovered(false)
                .with_branch_coverage(true)
                .with_line_numbers(false);

            assert_eq!(config.title, "Test");
            assert_eq!(config.theme, Theme::HighContrast);
            assert!(!config.highlight_uncovered);
            assert!(config.include_branch_coverage);
            assert!(!config.show_line_numbers);
        }
    }

    mod formatter_tests {
        use super::*;

        #[test]
        fn test_html_formatter_new() {
            let report = CoverageReport::new(10);
            let formatter = HtmlFormatter::new(&report);
            assert_eq!(formatter.config.title, "Coverage Report");
        }

        #[test]
        fn test_html_formatter_with_config() {
            let report = CoverageReport::new(10);
            let config = HtmlReportConfig::new().with_title("Custom Title");
            let formatter = HtmlFormatter::with_config(&report, config);
            assert_eq!(formatter.config.title, "Custom Title");
        }

        #[test]
        fn test_generate_contains_html_structure() {
            let report = create_test_report();
            let formatter = HtmlFormatter::new(&report);
            let output = formatter.generate();

            assert!(output.contains("<!DOCTYPE html>"));
            assert!(output.contains("<html"));
            assert!(output.contains("</html>"));
            assert!(output.contains("<head>"));
            assert!(output.contains("<body"));
            assert!(output.contains("<style>"));
        }

        #[test]
        fn test_generate_contains_title() {
            let report = create_test_report();
            let config = HtmlReportConfig::new().with_title("My Coverage");
            let formatter = HtmlFormatter::with_config(&report, config);
            let output = formatter.generate();

            assert!(output.contains("<title>My Coverage</title>"));
        }

        #[test]
        fn test_generate_contains_summary() {
            let report = create_test_report();
            let formatter = HtmlFormatter::new(&report);
            let output = formatter.generate();

            assert!(output.contains("Total Blocks"));
            assert!(output.contains("Covered"));
            assert!(output.contains("Coverage"));
        }

        #[test]
        fn test_generate_contains_files() {
            let report = create_test_report();
            let formatter = HtmlFormatter::new(&report);
            let output = formatter.generate();

            assert!(output.contains("src/game.rs"));
            assert!(output.contains("src/player.rs"));
        }

        #[test]
        fn test_theme_class() {
            let report = CoverageReport::new(0);

            let light = HtmlFormatter::with_config(
                &report,
                HtmlReportConfig::new().with_theme(Theme::Light),
            );
            assert_eq!(light.theme_class(), "theme-light");

            let dark = HtmlFormatter::with_config(
                &report,
                HtmlReportConfig::new().with_theme(Theme::Dark),
            );
            assert_eq!(dark.theme_class(), "theme-dark");

            let hc = HtmlFormatter::with_config(
                &report,
                HtmlReportConfig::new().with_theme(Theme::HighContrast),
            );
            assert_eq!(hc.theme_class(), "theme-high-contrast");
        }

        #[test]
        fn test_save_creates_directory_and_file() {
            let report = create_test_report();
            let formatter = HtmlFormatter::new(&report);

            let temp_dir = tempfile::tempdir().unwrap();
            let output_dir = temp_dir.path().join("coverage_report");

            formatter.save(&output_dir).unwrap();

            assert!(output_dir.exists());
            assert!(output_dir.join("index.html").exists());
        }
    }

    mod theme_tests {
        use super::*;

        #[test]
        fn test_theme_default() {
            let theme = Theme::default();
            assert_eq!(theme, Theme::Light);
        }

        #[test]
        fn test_theme_variants() {
            let _ = Theme::Light;
            let _ = Theme::Dark;
            let _ = Theme::HighContrast;
        }
    }
}
