use std::{collections::BTreeMap, time::{Duration, UNIX_EPOCH}};

use crate::{
    automation,
    error::HenError,
    request::{AssertionOutcome, AssertionStatus, ExecutionRecord, RequestFailure, RequestFailureKind},
};

#[derive(Debug, Clone)]
struct JUnitCase {
    class_name: String,
    name: String,
    time_secs: f64,
    status: JUnitCaseStatus,
}

#[derive(Debug, Clone)]
enum JUnitCaseStatus {
    Passed,
    Failure {
        kind: String,
        message: String,
        details: String,
    },
    Skipped {
        message: String,
    },
    Error {
        kind: String,
        message: String,
        details: String,
    },
}

pub fn run_outcome_junit(outcome: &automation::RunOutcome) -> String {
    let suite_name = report_suite_name(
        &outcome.collection.name,
        &outcome.collection.path.display().to_string(),
    );

    let mut records_by_index: BTreeMap<usize, &ExecutionRecord> = outcome
        .records
        .iter()
        .map(|record| (record.index, record))
        .collect();
    let mut failures_by_index: BTreeMap<usize, &RequestFailure> = outcome
        .failures
        .iter()
        .filter_map(|failure| failure.index().map(|index| (index, failure)))
        .collect();
    let mut cases = Vec::new();
    let request_summaries: BTreeMap<usize, &automation::RequestSummary> = outcome
        .collection
        .requests
        .iter()
        .map(|request| (request.index, request))
        .collect();

    for index in &outcome.plan {
        if let Some(record) = records_by_index.remove(index) {
            cases.extend(record_cases(
                &suite_name,
                request_summaries.get(index).copied(),
                record,
            ));
            continue;
        }

        if let Some(failure) = failures_by_index.remove(index) {
            cases.extend(failure_cases(
                &suite_name,
                request_summaries.get(index).copied(),
                failure,
            ));
        }
    }

    cases.extend(records_by_index.into_values().flat_map(|record| {
        record_cases(
            &suite_name,
            request_summaries.get(&record.index).copied(),
            record,
        )
    }));
    cases.extend(failures_by_index.into_values().flat_map(|failure| {
        failure_cases(
            &suite_name,
            failure
                .index()
                .and_then(|index| request_summaries.get(&index).copied()),
            failure,
        )
    }));
    cases.extend(
        outcome
            .failures
            .iter()
            .filter(|failure| failure.index().is_none())
            .flat_map(|failure| failure_cases(&suite_name, None, failure)),
    );

    if let Some(signal) = outcome.interrupted {
        cases.push(JUnitCase {
            class_name: suite_name.clone(),
            name: format!("run interrupted by {}", signal.as_str()),
            time_secs: 0.0,
            status: JUnitCaseStatus::Error {
                kind: "interrupted".to_string(),
                message: format!("Execution interrupted by {}", signal.as_str()),
                details: format!(
                    "Execution interrupted by {} before completing all planned requests",
                    signal.as_str()
                ),
            },
        });
    }

    render_junit_suite(
        &suite_name,
        total_elapsed(&outcome.records)
            .unwrap_or_default()
            .as_secs_f64(),
        &[
            ("path", outcome.collection.path.display().to_string()),
            (
                "selectedRequests",
                outcome
                    .selected_requests
                    .iter()
                    .map(|index| index.to_string())
                    .collect::<Vec<_>>()
                    .join(","),
            ),
            (
                "plan",
                outcome
                    .plan
                    .iter()
                    .map(|index| index.to_string())
                    .collect::<Vec<_>>()
                    .join(","),
            ),
            ("executionFailed", outcome.execution_failed.to_string()),
            ("interrupted", outcome.interrupted.is_some().to_string()),
            (
                "interruptSignal",
                outcome
                    .interrupted
                    .map(|signal| signal.as_str().to_string())
                    .unwrap_or_default(),
            ),
        ],
        &cases,
    )
}

pub fn verification_result_junit(result: &automation::VerificationResult) -> String {
    let fallback = result
        .path
        .as_ref()
        .map(|path| path.display().to_string())
        .unwrap_or_else(|| "hen verify".to_string());
    let suite_name = report_suite_name(&result.summary.name, &fallback);

    render_junit_suite(
        &suite_name,
        0.0,
        &[("path", fallback.clone())],
        &[JUnitCase {
            class_name: suite_name.clone(),
            name: "syntax validation".to_string(),
            time_secs: 0.0,
            status: JUnitCaseStatus::Passed,
        }],
    )
}

pub fn hen_error_junit(suite_name: &str, case_name: &str, error: &HenError) -> String {
    render_junit_suite(
        suite_name,
        0.0,
        &[],
        &[JUnitCase {
            class_name: suite_name.to_string(),
            name: case_name.to_string(),
            time_secs: 0.0,
            status: JUnitCaseStatus::Error {
                kind: error.kind().label().to_string(),
                message: error.summary().to_string(),
                details: error.to_string(),
            },
        }],
    )
}

fn success_case(
    suite_name: &str,
    request_summary: Option<&automation::RequestSummary>,
    record: &ExecutionRecord,
) -> JUnitCase {
    JUnitCase {
        class_name: suite_name.to_string(),
        name: request_case_name(
            record.index,
            request_summary,
            record.method.as_str(),
            &record.url,
            &record.description,
        ),
        time_secs: record.duration.as_secs_f64(),
        status: JUnitCaseStatus::Passed,
    }
}

fn record_cases(
    suite_name: &str,
    request_summary: Option<&automation::RequestSummary>,
    record: &ExecutionRecord,
) -> Vec<JUnitCase> {
    let request_name = request_case_name(
        record.index,
        request_summary,
        record.method.as_str(),
        &record.url,
        &record.description,
    );

    if record.execution.assertions.is_empty() {
        return vec![success_case(suite_name, request_summary, record)];
    }

    let case_time_secs = record.duration.as_secs_f64() / record.execution.assertions.len() as f64;

    assertion_cases(
        suite_name,
        &request_name,
        &record.execution.assertions,
        case_time_secs,
        "assertion",
    )
}

fn failure_case(
    suite_name: &str,
    request_summary: Option<&automation::RequestSummary>,
    failure: &RequestFailure,
) -> JUnitCase {
    let name = match failure.index() {
        Some(index) => request_case_name(
            index,
            request_summary,
            request_summary
                .map(|summary| summary.method.as_str())
                .unwrap_or("request"),
            request_summary
                .map(|summary| summary.url.as_str())
                .unwrap_or(""),
            failure.request(),
        ),
        None => failure.request().to_string(),
    };

    let status = match failure.kind() {
        RequestFailureKind::Execution { message, .. } => JUnitCaseStatus::Failure {
            kind: "execution".to_string(),
            message: message.clone(),
            details: failure.to_string(),
        },
        RequestFailureKind::Dependency { dependency } => JUnitCaseStatus::Skipped {
            message: format!("dependency '{}' failed", dependency),
        },
        RequestFailureKind::MissingDependency { dependency } => JUnitCaseStatus::Skipped {
            message: format!("missing dependency '{}'", dependency),
        },
        RequestFailureKind::MapAborted { group, cause } => JUnitCaseStatus::Skipped {
            message: format!("'{}' aborted after failure in '{}'", group, cause),
        },
        RequestFailureKind::Join { message } => JUnitCaseStatus::Error {
            kind: "join".to_string(),
            message: message.clone(),
            details: failure.to_string(),
        },
    };

    JUnitCase {
        class_name: suite_name.to_string(),
        name,
        time_secs: 0.0,
        status,
    }
}

fn failure_cases(
    suite_name: &str,
    request_summary: Option<&automation::RequestSummary>,
    failure: &RequestFailure,
) -> Vec<JUnitCase> {
    match failure.kind() {
        RequestFailureKind::Execution { assertions, .. } if !assertions.is_empty() => {
            let request_name = match failure.index() {
                Some(index) => request_case_name(
                    index,
                    request_summary,
                    request_summary
                        .map(|summary| summary.method.as_str())
                        .unwrap_or("request"),
                    request_summary
                        .map(|summary| summary.url.as_str())
                        .unwrap_or(""),
                    failure.request(),
                ),
                None => failure.request().to_string(),
            };

            assertion_cases(suite_name, &request_name, assertions, 0.0, "assertion")
        }
        _ => vec![failure_case(suite_name, request_summary, failure)],
    }
}

fn assertion_cases(
    suite_name: &str,
    request_name: &str,
    assertions: &[AssertionOutcome],
    time_secs: f64,
    failure_kind: &str,
) -> Vec<JUnitCase> {
    assertions
        .iter()
        .map(|assertion| JUnitCase {
            class_name: suite_name.to_string(),
            name: assertion_case_name(request_name, &assertion.assertion),
            time_secs,
            status: assertion_status_to_junit(
                &assertion.status,
                &assertion.message,
                &assertion.assertion,
                failure_kind,
            ),
        })
        .collect()
}

fn assertion_status_to_junit(
    status: &AssertionStatus,
    message: &Option<String>,
    assertion: &str,
    failure_kind: &str,
) -> JUnitCaseStatus {
    match status {
        AssertionStatus::Passed => JUnitCaseStatus::Passed,
        AssertionStatus::Skipped => JUnitCaseStatus::Skipped {
            message: message
                .clone()
                .unwrap_or_else(|| "assertion skipped".to_string()),
        },
        AssertionStatus::Failed => {
            let details = message.clone().unwrap_or_else(|| assertion.to_string());
            JUnitCaseStatus::Failure {
                kind: failure_kind.to_string(),
                message: details.clone(),
                details,
            }
        }
    }
}

fn assertion_case_name(request_name: &str, assertion: &str) -> String {
    format!("{} :: {}", request_name, assertion)
}

fn request_case_name(
    index: usize,
    request_summary: Option<&automation::RequestSummary>,
    method: &str,
    url: &str,
    fallback: &str,
) -> String {
    match request_summary {
        Some(summary) => format!("#{} {} {}", summary.index, summary.method, summary.url),
        None if !url.is_empty() => format!("#{} {} {}", index, method, url),
        None => format!("#{} {}", index, fallback),
    }
}

fn render_junit_suite(
    suite_name: &str,
    suite_time_secs: f64,
    properties: &[(&str, String)],
    cases: &[JUnitCase],
) -> String {
    let failure_count = cases
        .iter()
        .filter(|case| matches!(case.status, JUnitCaseStatus::Failure { .. }))
        .count();
    let skipped_count = cases
        .iter()
        .filter(|case| matches!(case.status, JUnitCaseStatus::Skipped { .. }))
        .count();
    let error_count = cases
        .iter()
        .filter(|case| matches!(case.status, JUnitCaseStatus::Error { .. }))
        .count();

    let mut xml = String::new();
    xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
    xml.push_str(&format!(
        "<testsuite name=\"{}\" tests=\"{}\" failures=\"{}\" errors=\"{}\" skipped=\"{}\" time=\"{}\">",
        escape_xml(suite_name),
        cases.len(),
        failure_count,
        error_count,
        skipped_count,
        format_junit_time(suite_time_secs),
    ));

    if !properties.is_empty() {
        xml.push_str("\n  <properties>");
        for (name, value) in properties {
            xml.push_str(&format!(
                "\n    <property name=\"{}\" value=\"{}\"/>",
                escape_xml(name),
                escape_xml(value),
            ));
        }
        xml.push_str("\n  </properties>");
    }

    for case in cases {
        xml.push_str(&format!(
            "\n  <testcase classname=\"{}\" name=\"{}\" time=\"{}\">",
            escape_xml(&case.class_name),
            escape_xml(&case.name),
            format_junit_time(case.time_secs),
        ));

        match &case.status {
            JUnitCaseStatus::Passed => {}
            JUnitCaseStatus::Failure {
                kind,
                message,
                details,
            } => {
                xml.push_str(&format!(
                    "\n    <failure type=\"{}\" message=\"{}\">{}</failure>",
                    escape_xml(kind),
                    escape_xml(message),
                    escape_xml(details),
                ));
            }
            JUnitCaseStatus::Skipped { message } => {
                xml.push_str(&format!(
                    "\n    <skipped message=\"{}\"/>",
                    escape_xml(message),
                ));
            }
            JUnitCaseStatus::Error {
                kind,
                message,
                details,
            } => {
                xml.push_str(&format!(
                    "\n    <error type=\"{}\" message=\"{}\">{}</error>",
                    escape_xml(kind),
                    escape_xml(message),
                    escape_xml(details),
                ));
            }
        }

        xml.push_str("\n  </testcase>");
    }

    xml.push_str("\n</testsuite>\n");
    xml
}

fn report_suite_name(name: &str, fallback: &str) -> String {
    let trimmed = name.trim();
    if trimmed.is_empty() {
        fallback.to_string()
    } else {
        trimmed.to_string()
    }
}

fn format_junit_time(time_secs: f64) -> String {
    if time_secs == 0.0 {
        "0".to_string()
    } else {
        format!("{:.3}", time_secs)
    }
}

fn escape_xml(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&apos;")
}

fn total_elapsed(records: &[ExecutionRecord]) -> Option<Duration> {
    if records.is_empty() {
        return None;
    }

    let mut earliest: Option<Duration> = None;
    let mut latest: Option<Duration> = None;

    for record in records {
        let start = match record.started_at.duration_since(UNIX_EPOCH) {
            Ok(duration) => duration,
            Err(_) => continue,
        };

        let finish = match record
            .started_at
            .checked_add(record.duration)
            .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
        {
            Some(duration) => duration,
            None => continue,
        };

        earliest = Some(match earliest {
            Some(current) => current.min(start),
            None => start,
        });
        latest = Some(match latest {
            Some(current) => current.max(finish),
            None => finish,
        });
    }

    match (earliest, latest) {
        (Some(start), Some(end)) if end >= start => Some(end - start),
        _ => None,
    }
}