use std::num::TryFromIntError;

use chrono::TimeDelta;
use itertools::Itertools;
use log::{debug, warn};
use serde::Serialize;
use thiserror::Error;
#[cfg(feature = "bindings")]
use ts_rs::TS;

use crate::{
    flight::Flight,
    geo::{Degrees, FloatMeters},
    igc::IgcFix,
    opti::TaskOptimizationResult,
    serde::serialize_timedelta_as_seconds,
    task::{EarthModel, GoalKind, Task},
    turnpoint::{Turnpoint, TurnpointRole},
};

#[derive(Error, Debug)]
pub enum ScoreError {
    #[error("Task is not completed")]
    NotCompleted,

    #[error("Task completion time is invalid")]
    InvalidCompletionTime(TryFromIntError),

    #[error("Goal line direction is not defined")]
    UndefinedGoalLineDirection,
}

type TurnpointIndex = usize;
type Seconds = i32;

#[derive(Debug, Serialize, Copy, Clone)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct TurnpointTag {
    pub tp: TurnpointIndex,
    pub pos: IgcFix,
}

#[derive(Debug, Serialize)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct SpeedSectionAttempt {
    pub tags: Vec<TurnpointTag>,
}

impl SpeedSectionAttempt {
    pub fn elapsed_time(&self, task: &Task) -> Result<TimeDelta, ScoreError> {
        match self.speed_section_bounds(task) {
            Some((first, last)) => Ok(last.pos.time - first.pos.time),
            _ => Err(ScoreError::NotCompleted),
        }
    }

    pub fn mean_speed(
        &self,
        task: &Task,
        route: &TaskOptimizationResult,
    ) -> Result<f64, ScoreError> {
        let elapsed_time = self.elapsed_time(task)?;
        Ok(3.6 * route.speed_section.distance / elapsed_time.num_seconds() as f64)
    }

    pub fn speed_section_bounds(&self, task: &Task) -> Option<(TurnpointTag, TurnpointTag)> {
        let start = self
            .tags
            .iter()
            .find(|tag| task.turnpoints[tag.tp].role == TurnpointRole::Sss)?;
        let end = self.tags.iter().find(|tag| {
            matches!(
                task.turnpoints[tag.tp].role,
                TurnpointRole::Ess | TurnpointRole::EssAndGoal
            )
        })?;
        Some((*start, *end))
    }

    pub fn interval_contains(&self, other: &SpeedSectionAttempt, task: &Task) -> bool {
        let (self_start, self_end) = match self.speed_section_bounds(task) {
            Some(bounds) => bounds,
            None => return false,
        };
        let (other_start, other_end) = match other.speed_section_bounds(task) {
            Some(bounds) => bounds,
            None => return false,
        };

        self_start.pos.time >= other_start.pos.time && self_end.pos.time <= other_end.pos.time
    }
}

#[derive(Debug, Serialize)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct IncompleteSpeedSectionAttempt {
    pub tags: Vec<TurnpointTag>,
    pub miss_tp_idx: TurnpointIndex,
    pub miss_distance: FloatMeters,
    pub miss_fix: IgcFix,
}

#[derive(Debug, Serialize)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct CompleteSpeedSectionAttempt {
    #[cfg_attr(feature = "bindings", ts(as = "i32"))]
    #[serde(serialize_with = "serialize_timedelta_as_seconds")]
    pub duration: TimeDelta,
    pub attempt: SpeedSectionAttempt,
}

#[derive(Debug, Serialize)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct CompleteSpeedSectionAttempts {
    pub attempts: Vec<CompleteSpeedSectionAttempt>,
}

#[derive(Debug)]
pub enum TaskResult {
    NotCompleted,
    Completed(SpeedSectionAttempt),
}

#[derive(Debug, Serialize)]
#[serde(tag = "variant")]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub enum DetailedTaskResult {
    NotCompleted(IncompleteSpeedSectionAttempt),
    Completed(CompleteSpeedSectionAttempts),
}

impl TaskResult {
    pub fn total_seconds(&self, task: &Task) -> Result<Seconds, ScoreError> {
        match self {
            TaskResult::NotCompleted => Err(ScoreError::NotCompleted),
            TaskResult::Completed(attempt) => {
                let seconds = attempt.elapsed_time(task)?.num_seconds();
                match Seconds::try_from(seconds) {
                    Ok(value) => Ok(value),
                    Err(e) => Err(ScoreError::InvalidCompletionTime(e)),
                }
            }
        }
    }
}

pub fn score_flight_on_task(task: &Task, flight: &Flight) -> TaskResult {
    let tags = find_all_turnpoint_tags(task, flight);
    find_shortest_speed_section(task, &tags)
}

pub fn score_flight_on_task_detailed(
    task: &Task,
    flight: &Flight,
) -> Result<DetailedTaskResult, ScoreError> {
    let tags = find_all_turnpoint_tags(task, flight);

    let attempts = find_speed_section_attempts(task, &tags);
    if !attempts.is_empty() {
        return Ok(DetailedTaskResult::Completed(deduplicate_attempts(
            task, attempts,
        )?));
    }

    let incomplete_attempts = find_incomplete_speed_section_attempts(task, &tags, flight);
    match incomplete_attempts.into_iter().next() {
        Some(attempt) => Ok(DetailedTaskResult::NotCompleted(attempt)),
        None => Ok(DetailedTaskResult::NotCompleted(find_miss_distance(
            task,
            &SpeedSectionAttempt { tags: vec![] },
            flight,
            first_navigation_tp_idx(task),
        ))),
    }
}

// Deduplicates attempts by removing those which are supersets of shorter attempts.
//
// Reference attempt             [=======]
//                               :       :
// Entirely before    [=====]    :       :             Kept
//                               :       :
// Overlapping start       [=====:=]     :             Kept
//                               :       :
// Overlapping end               : [=====:=]           Kept
//                               :       :
// Entirely after                :       :   [=====]   Kept
//                               :       :
// Contained by              [===:=======:===]         To be discarded
//                               :       :
// Contains                      : [===] :             Cannot happen if sorted by duration first
fn deduplicate_attempts(
    task: &Task,
    all_attempts: Vec<SpeedSectionAttempt>,
) -> Result<CompleteSpeedSectionAttempts, ScoreError> {
    let mut attempts: Vec<CompleteSpeedSectionAttempt> = vec![];

    for attempt in all_attempts
        .into_iter()
        .sorted_by_key(|a| a.elapsed_time(task).ok())
    {
        if !attempts
            .iter()
            .any(|other| other.attempt.interval_contains(&attempt, task))
        {
            attempts.push(CompleteSpeedSectionAttempt {
                duration: attempt.elapsed_time(task)?,
                attempt,
            });
        }
    }

    Ok(CompleteSpeedSectionAttempts { attempts })
}

fn find_all_turnpoint_tags(task: &Task, flight: &Flight) -> Vec<TurnpointTag> {
    task.turnpoints
        .iter()
        .enumerate()
        .flat_map(|(idx, turnpoint)| find_tags_for_turnpoint(turnpoint, idx, flight, task))
        .sorted_by_key(|tag| tag.pos.time)
        .collect()
}

/// Finds the points where the flight crosses the turnpoint's circumference.
/// It iterates through the flight positions and checks if the position is within the turnpoint's radius.
/// If there is a transition from inside to outside or vice versa, it records the crossing as a "tag".
fn find_tags_for_turnpoint(
    turnpoint: &Turnpoint,
    turnpoint_idx: usize,
    flight: &Flight,
    task: &Task,
) -> Vec<TurnpointTag> {
    let is_tp_goal = matches!(
        turnpoint.role,
        TurnpointRole::Goal | TurnpointRole::EssAndGoal
    );
    let is_goal_line = task
        .goal
        .as_ref()
        .is_some_and(|g| g.kind == Some(GoalKind::Line));
    if is_tp_goal && is_goal_line {
        return find_tags_for_line(turnpoint, turnpoint_idx, flight, task);
    }

    find_boundary_crossings(turnpoint_idx, &flight.positions, |pos| {
        is_within_radius(pos, turnpoint, &task.model)
    })
}

fn find_tags_for_line(
    turnpoint: &Turnpoint,
    turnpoint_idx: usize,
    flight: &Flight,
    task: &Task,
) -> Vec<TurnpointTag> {
    let line_azimuth = match task.find_goal_azimuth() {
        Ok(azi) => azi,
        Err(e) => {
            warn!("Could not find line tags: {e:?}");
            return Vec::new();
        }
    };

    find_boundary_crossings(turnpoint_idx, &flight.positions, |pos| {
        is_within_line(pos, turnpoint, &task.model, line_azimuth)
    })
}

/// Generic helper to find boundary crossings given a predicate function.
/// Records positions where the predicate changes from true to false or vice versa.
fn find_boundary_crossings<F>(
    turnpoint_idx: usize,
    positions: &[IgcFix],
    is_inside: F,
) -> Vec<TurnpointTag>
where
    F: Fn(&IgcFix) -> bool,
{
    let mut positions_iter = positions.iter();

    // Get the first position, or return early if empty
    let Some(first_pos) = positions_iter.next() else {
        return Vec::new();
    };

    let mut tags = Vec::new();
    let mut was_inside = is_inside(first_pos);

    for position in positions_iter {
        let now_inside = is_inside(position);

        if now_inside != was_inside {
            was_inside = now_inside;
            tags.push(TurnpointTag {
                tp: turnpoint_idx,
                pos: *position,
            });
        }
    }

    debug!("tags for {turnpoint_idx}: {tags:?}");
    tags
}

fn is_within_radius(position: &IgcFix, turnpoint: &Turnpoint, model: &EarthModel) -> bool {
    let distance = model.distance(
        position.lat,
        position.lon,
        turnpoint.waypoint.lat,
        turnpoint.waypoint.lon,
    );
    distance <= turnpoint.radius
}

fn is_within_line(
    position: &IgcFix,
    turnpoint: &Turnpoint,
    model: &EarthModel,
    line_azimuth: Degrees,
) -> bool {
    // First, check whether the position is within the turnpoint radius.
    let distance = model.distance(
        position.lat,
        position.lon,
        turnpoint.waypoint.lat,
        turnpoint.waypoint.lon,
    );
    if distance > turnpoint.radius {
        return false;
    }
    // Only if the previous check succeeded, check if the heading between the goal
    // center and the current position is within 90 degrees of the line heading, to
    // make sure the position is actually within the semi-circle after the goal line.
    // This check is somewhat expensive, so we skip it if the position is already
    // outside the full cylinder.
    let position_heading = model.heading(
        turnpoint.waypoint.lat,
        turnpoint.waypoint.lon,
        position.lat,
        position.lon,
    );
    (position_heading - line_azimuth).abs() < 90.0
}

fn find_shortest_speed_section(task: &Task, tags: &[TurnpointTag]) -> TaskResult {
    let attempts = find_speed_section_attempts(task, tags);

    debug!("found {} task attempts: {:#?}", attempts.len(), attempts);

    let best_attempt = attempts
        .into_iter()
        .min_by_key(|attempt| attempt.elapsed_time(task).ok());

    match best_attempt {
        Some(attempt) => TaskResult::Completed(attempt),
        None => TaskResult::NotCompleted,
    }
}

fn find_speed_section_attempts(task: &Task, tags: &[TurnpointTag]) -> Vec<SpeedSectionAttempt> {
    let first_navigation_tp_idx = first_navigation_tp_idx(task);
    tags.iter()
        .enumerate()
        // Look for a complete attempt for each tag of the first navigation turnpoint.
        .filter(|(_, tag)| tag.tp == first_navigation_tp_idx)
        .filter_map(|(i, _)| build_attempt(task, tags, i))
        .collect()
}

fn build_attempt(
    task: &Task,
    tags: &[TurnpointTag],
    start_idx: usize,
) -> Option<SpeedSectionAttempt> {
    let mut attempt = SpeedSectionAttempt {
        tags: vec![tags[start_idx]],
    };
    let mut prev_tp = tags[start_idx].tp;

    // Look at all remaining turnpoint tags.
    // If there's a sequence of increasing tags which reach the goal, add the
    // attempt.
    for current in &tags[start_idx + 1..] {
        let is_same_tp_but_later_sss =
            current.tp == prev_tp && task.turnpoints[current.tp].role == TurnpointRole::Sss;
        if is_same_tp_but_later_sss {
            // We know tags is non-empty (has at least initial tag)
            let last_idx = attempt.tags.len() - 1;
            attempt.tags[last_idx] = *current;
            continue;
        }

        // Skip tags which are not the current turnpoint.
        let is_next_tp = current.tp == prev_tp + 1;
        if !is_next_tp {
            continue;
        }

        // Tag the current turnpoint
        attempt.tags.push(*current);
        prev_tp = current.tp;

        // Reached goal, attempt is complete
        if matches!(
            task.turnpoints[current.tp].role,
            TurnpointRole::Goal | TurnpointRole::EssAndGoal
        ) {
            return Some(attempt);
        }
    }

    None
}

fn find_incomplete_speed_section_attempts(
    task: &Task,
    tags: &[TurnpointTag],
    flight: &Flight,
) -> Vec<IncompleteSpeedSectionAttempt> {
    let first_navigation_tp_idx = first_navigation_tp_idx(task);

    // Build a list of all non-completed attempts
    let mut attempts: Vec<_> = tags
        .iter()
        .enumerate()
        .filter(|(_, tag)| tag.tp == first_navigation_tp_idx)
        .filter_map(|(i, _)| build_incomplete_attempt(task, tags, i))
        .collect();

    // Keep only attempts tagging the maximum number of turnpoints
    if let Some(max_len) = attempts.iter().map(|a| a.tags.len()).max() {
        attempts.retain(|a| a.tags.len() == max_len);
    }

    // Find miss distances for remaining attempts
    let mut incomplete_attempts: Vec<_> = attempts
        .iter()
        .map(|attempt| find_miss_distance(task, attempt, flight, first_navigation_tp_idx))
        .collect();

    incomplete_attempts.sort_by(|a, b| match a.miss_distance.partial_cmp(&b.miss_distance) {
        Some(ord) => ord,
        None => std::cmp::Ordering::Equal,
    });

    incomplete_attempts
}

// TODO Can this be refactored with build_attempt ?
fn build_incomplete_attempt(
    task: &Task,
    tags: &[TurnpointTag],
    start_idx: usize,
) -> Option<SpeedSectionAttempt> {
    let mut attempt = SpeedSectionAttempt {
        tags: vec![tags[start_idx]],
    };
    let mut prev_tp = tags[start_idx].tp;

    // Look at all remaining turnpoint tags.
    // If there's a sequence of increasing tags which reach the goal, add the
    // attempt.
    for current in &tags[start_idx + 1..] {
        let is_same_tp_but_later_sss =
            current.tp == prev_tp && task.turnpoints[current.tp].role == TurnpointRole::Sss;
        if is_same_tp_but_later_sss {
            // We know tags is non-empty (has at least initial tag)
            let last_idx = attempt.tags.len() - 1;
            attempt.tags[last_idx] = *current;
            continue;
        }

        // Skip tags which are not the current turnpoint.
        let is_next_tp = current.tp == prev_tp + 1;
        if !is_next_tp {
            continue;
        }

        // Tag the current turnpoint
        attempt.tags.push(*current);
        prev_tp = current.tp;
    }

    Some(attempt)
}

fn find_miss_distance(
    task: &Task,
    attempt: &SpeedSectionAttempt,
    flight: &Flight,
    first_navigation_tp_idx: TurnpointIndex,
) -> IncompleteSpeedSectionAttempt {
    let miss_tp_idx = first_navigation_tp_idx + attempt.tags.len();
    let missed_tp = &task.turnpoints[miss_tp_idx];
    let (miss_distance, miss_fix_idx) = flight
        .positions
        .iter()
        .map(|fix| {
            (task.model.distance(
                fix.lat,
                fix.lon,
                missed_tp.waypoint.lat,
                missed_tp.waypoint.lon,
            ) - missed_tp.radius)
                .abs()
        })
        .enumerate()
        .fold(
            (FloatMeters::INFINITY, 0),
            |(min_dist, best_idx), (idx, dist)| {
                if dist < min_dist {
                    (dist, idx)
                } else {
                    (min_dist, best_idx)
                }
            },
        );

    IncompleteSpeedSectionAttempt {
        tags: attempt.tags.clone(),
        miss_tp_idx,
        miss_distance,
        miss_fix: flight.positions[miss_fix_idx],
    }
}

fn first_navigation_tp_idx(task: &Task) -> TurnpointIndex {
    task.turnpoints
        .iter()
        // From XCTrack Competition Interfaces:
        // TAKEOFF type can be used only for the first turnpoint and is not required.
        // If present, means that the first turnpoint is not used for navigation.
        .position(|tp| tp.role != TurnpointRole::Takeoff)
        .unwrap_or(0)
}

#[cfg(test)]
mod tests {
    use std::ops::Add;

    use chrono::{NaiveDate, NaiveTime};

    use super::*;
    use crate::{
        flight::Flight,
        igc::IgcFix,
        task::{EarthModel, Task},
        turnpoint::{Turnpoint, TurnpointRole, Waypoint},
    };

    fn create_waypoint(name: &str, lat: f64, lon: f64) -> Waypoint {
        Waypoint {
            name: name.to_string(),
            description: None,
            lat,
            lon,
            alt: 0.0,
        }
    }

    fn create_turnpoint(
        name: &str,
        lat: f64,
        lon: f64,
        radius: f64,
        role: TurnpointRole,
    ) -> Turnpoint {
        Turnpoint {
            waypoint: create_waypoint(name, lat, lon),
            radius,
            role,
        }
    }

    fn create_fix(lat: f64, lon: f64, hour: u32, minute: u32, second: u32) -> IgcFix {
        let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
        let time = NaiveTime::from_hms_opt(hour, minute, second).unwrap();
        IgcFix {
            time: date.and_time(time).and_utc(),
            lat,
            lon,
            alt: 1000,
        }
    }

    #[test]
    fn score_empty_state() {
        let task = Task::new();
        let flight = Flight::default();
        let result = score_flight_on_task(&task, &flight);
        assert!(matches!(result, TaskResult::NotCompleted));
    }

    #[test]
    fn test_no_turnpoints_tagged() {
        // Create a simple task with SSS at (0, 0) and ESS at (10, 0)
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![
            create_turnpoint("SSS", 0.0, 0.0, 1.0, TurnpointRole::Sss),
            create_turnpoint("ESS", 10.0, 0.0, 1.0, TurnpointRole::EssAndGoal),
        ];

        // Create a flight that never reaches any turnpoint
        // Flying far away from the task
        let mut flight = Flight::default();
        flight.positions = vec![
            create_fix(50.0, 50.0, 0, 0, 0),
            create_fix(51.0, 51.0, 0, 1, 0),
            create_fix(52.0, 52.0, 0, 2, 0),
        ];

        let result = score_flight_on_task_detailed(&task, &flight);

        match result {
            Ok(DetailedTaskResult::NotCompleted(incomplete)) => {
                // Should have no tags (didn't reach any turnpoints)
                assert_eq!(incomplete.tags.len(), 0);
                // Should be missing the first navigation turnpoint (SSS)
                assert_eq!(incomplete.miss_tp_idx, 0);
                // Miss distance should be large (roughly 70 in Cartesian space)
                assert_eq!(incomplete.miss_distance.round(), 70.0);
                // Miss fix is the first one
                assert_eq!(incomplete.miss_fix.lat, 50.0);
                assert_eq!(incomplete.miss_fix.lon, 50.0);
            }
            _ => {
                panic!("Expected NotCompleted result");
            }
        }
    }

    #[test]
    fn test_tags_sss_but_misses_first_turnpoint() {
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![
            create_turnpoint("SSS", 0.0, 0.0, 1.0, TurnpointRole::Sss),
            create_turnpoint("TP1", 5.0, 0.0, 1.0, TurnpointRole::Turnpoint),
            create_turnpoint("ESS", 10.0, 0.0, 1.0, TurnpointRole::EssAndGoal),
        ];

        // Flight enters SSS but doesn't reach TP1
        let mut flight = Flight::default();
        flight.positions = vec![
            create_fix(-2.0, 0.0, 0, 0, 0), // Before SSS
            create_fix(0.5, 0.0, 0, 1, 0),  // Inside SSS
            create_fix(2.0, 0.0, 0, 2, 0),  // Past SSS, approaching TP1
            create_fix(3.0, 0.0, 0, 3, 0),  // Closest to TP1 (distance = 2.0 - 1.0 = 1.0)
            create_fix(2.0, 0.0, 0, 4, 0),  // Turning back
        ];

        let result = score_flight_on_task_detailed(&task, &flight);

        match result {
            Ok(DetailedTaskResult::NotCompleted(incomplete)) => {
                assert_eq!(incomplete.tags.len(), 1);
                assert_eq!(incomplete.tags[0].tp, 0); // Tagged SSS
                assert_eq!(incomplete.miss_tp_idx, 1); // Missing TP1
                assert_eq!(incomplete.miss_distance, 1.0); // 2.0 away from center with 1.0 radius
                assert_eq!(incomplete.miss_fix.lat, 3.0);
                assert_eq!(incomplete.miss_fix.lon, 0.0);
            }
            _ => {
                panic!("Expected NotCompleted result");
            }
        }
    }

    #[test]
    fn test_tags_two_turnpoints_misses_third() {
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![
            create_turnpoint("SSS", 0.0, 0.0, 1.0, TurnpointRole::Sss),
            create_turnpoint("TP1", 5.0, 0.0, 1.0, TurnpointRole::Turnpoint),
            create_turnpoint("TP2", 10.0, 0.0, 1.0, TurnpointRole::Turnpoint),
            create_turnpoint("ESS", 15.0, 0.0, 1.0, TurnpointRole::EssAndGoal),
        ];

        // Flight tags SSS and TP1, gets close to TP2 but doesn't tag it
        let mut flight = Flight::default();
        flight.positions = vec![
            create_fix(-1.0, 0.0, 0, 0, 0), // Before SSS
            create_fix(0.5, 0.0, 0, 1, 0),  // Inside SSS
            create_fix(5.5, 0.0, 0, 2, 0),  // Inside TP1
            create_fix(8.0, 0.0, 0, 3, 0),  // Approaching TP2 (distance = 2.0 - 1.0 = 1.0)
            create_fix(7.0, 0.0, 0, 4, 0),  // Turning back
        ];

        let result = score_flight_on_task_detailed(&task, &flight);

        match result {
            Ok(DetailedTaskResult::NotCompleted(incomplete)) => {
                assert_eq!(incomplete.tags.len(), 2);
                assert_eq!(incomplete.tags[0].tp, 0); // SSS
                assert_eq!(incomplete.tags[1].tp, 1); // TP1
                assert_eq!(incomplete.miss_tp_idx, 2); // Missing TP2
                assert_eq!(incomplete.miss_distance, 1.0);
                assert_eq!(incomplete.miss_fix.lat, 8.0);
            }
            _ => {
                panic!("Expected NotCompleted result");
            }
        }
    }

    #[test]
    fn test_completes_task_single_attempt() {
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![
            create_turnpoint("SSS", 0.0, 0.0, 1.0, TurnpointRole::Sss),
            create_turnpoint("TP1", 5.0, 0.0, 1.0, TurnpointRole::Turnpoint),
            create_turnpoint("ESS", 10.0, 0.0, 1.0, TurnpointRole::EssAndGoal),
        ];

        // Flight completes the task
        let mut flight = Flight::default();
        flight.positions = vec![
            create_fix(0.0, 0.0, 0, 0, 0),
            create_fix(1.5, 0.0, 0, 1, 0),  // Tag SSS
            create_fix(5.0, 0.0, 0, 2, 0),  // Tag TP1
            create_fix(10.0, 0.0, 0, 3, 0), // Tag ESS/Goal
        ];

        let result = score_flight_on_task_detailed(&task, &flight);

        match result {
            Ok(DetailedTaskResult::Completed(completion)) => {
                assert_eq!(completion.attempts.len(), 1);
                assert_eq!(completion.attempts[0].attempt.tags.len(), 3);
                assert_eq!(completion.attempts[0].attempt.tags[0].tp, 0); // SSS
                assert_eq!(completion.attempts[0].attempt.tags[1].tp, 1); // TP1
                assert_eq!(completion.attempts[0].attempt.tags[2].tp, 2); // ESS/Goal
                assert_eq!(completion.attempts[0].duration, TimeDelta::minutes(2));
            }
            _ => {
                panic!("Expected Completed result");
            }
        }
    }

    #[test]
    fn test_completes_task_distinct_attempts() {
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![
            create_turnpoint("SSS", 0.0, 0.0, 1.0, TurnpointRole::Sss),
            create_turnpoint("TP1", 5.0, 0.0, 1.0, TurnpointRole::Turnpoint),
            create_turnpoint("ESS", 10.0, 0.0, 1.0, TurnpointRole::EssAndGoal),
        ];

        // Flight completes the task twice, and the second attempt is faster.
        let mut flight = Flight::default();
        flight.positions = vec![
            create_fix(0.0, 0.0, 0, 0, 0),
            create_fix(1.5, 0.0, 1, 0, 0),   // Tag SSS #1
            create_fix(5.0, 0.0, 2, 0, 0),   // Tag TP1 #1
            create_fix(10.0, 0.0, 3, 0, 0),  // Tag ESS/Goal #1
            create_fix(0.0, 0.0, 4, 0, 0),   // Exit ESS/Goal
            create_fix(1.5, 0.0, 4, 10, 0),  // Tag SSS #2
            create_fix(5.0, 0.0, 4, 20, 0),  // Tag TP1 #2
            create_fix(10.0, 0.0, 4, 30, 0), // Tag ESS/Goal #2
        ];

        let result = score_flight_on_task_detailed(&task, &flight);

        match result {
            Ok(DetailedTaskResult::Completed(completion)) => {
                assert_eq!(completion.attempts.len(), 2);
                assert_eq!(
                    completion.attempts[0].attempt.elapsed_time(&task).unwrap(),
                    TimeDelta::minutes(20)
                );
                assert_eq!(
                    completion.attempts[1].attempt.elapsed_time(&task).unwrap(),
                    TimeDelta::hours(2)
                );
            }
            _ => {
                panic!("Expected Completed result");
            }
        }
    }

    #[test]
    fn test_deduplicate_overlapping_attempts_multistart() {
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![
            create_turnpoint("SSS", 0.0, 0.0, 1.0, TurnpointRole::Sss),
            create_turnpoint("TP1", 5.0, 0.0, 1.0, TurnpointRole::Turnpoint),
            create_turnpoint("ESS", 10.0, 0.0, 1.0, TurnpointRole::EssAndGoal),
        ];

        // Flight completes the task twice, with two SSS tags and a single ESS/Goal.
        let mut flight = Flight::default();
        flight.positions = vec![
            create_fix(0.0, 0.0, 0, 0, 0),
            create_fix(1.5, 0.0, 1, 0, 0),  // Tag SSS #1
            create_fix(5.0, 0.0, 2, 0, 0),  // Tag TP1 #1
            create_fix(0.0, 0.0, 3, 0, 0),  // Exit TP1
            create_fix(1.5, 0.0, 3, 10, 0), // Tag SSS #2
            create_fix(5.0, 0.0, 4, 0, 0),  // Tag TP1 #2
            create_fix(10.0, 0.0, 5, 0, 0), // Tag ESS/Goal #1 & #2
        ];

        let result = score_flight_on_task_detailed(&task, &flight);

        match result {
            Ok(DetailedTaskResult::Completed(completion)) => {
                assert_eq!(completion.attempts.len(), 1);
                assert_eq!(
                    completion.attempts[0].attempt.elapsed_time(&task).unwrap(),
                    TimeDelta::hours(1).add(TimeDelta::minutes(50))
                );
            }
            _ => {
                panic!("Expected Completed result");
            }
        }
    }

    #[test]
    fn test_deduplicate_overlapping_attempts_multiend() {
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![
            create_turnpoint("SSS", 0.0, 0.0, 1.0, TurnpointRole::Sss),
            create_turnpoint("TP1", 5.0, 0.0, 1.0, TurnpointRole::Turnpoint),
            create_turnpoint("ESS", 10.0, 0.0, 1.0, TurnpointRole::EssAndGoal),
        ];

        // Flight completes the task twice, with two SSS tags and a single ESS/Goal.
        let mut flight = Flight::default();
        flight.positions = vec![
            create_fix(0.0, 0.0, 0, 0, 0),
            create_fix(1.5, 0.0, 1, 0, 0),   // Tag SSS #1
            create_fix(5.0, 0.0, 2, 0, 0),   // Tag TP1 #1
            create_fix(10.0, 0.0, 3, 0, 0),  // Tag ESS/Goal #1
            create_fix(5.0, 0.0, 4, 20, 0),  // Tag TP1 #2
            create_fix(10.0, 0.0, 4, 30, 0), // Tag ESS/Goal #2
        ];

        let result = score_flight_on_task_detailed(&task, &flight);

        match result {
            Ok(DetailedTaskResult::Completed(completion)) => {
                assert_eq!(completion.attempts.len(), 1);
                assert_eq!(completion.attempts[0].duration, TimeDelta::hours(2));
            }
            _ => {
                panic!("Expected Completed result");
            }
        }
    }

    #[test]
    fn test_deduplicate_overlapping_attempts_tangled() {
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![
            create_turnpoint("SSS", 0.0, 0.0, 1.0, TurnpointRole::Sss),
            create_turnpoint("TP1", 5.0, 0.0, 1.0, TurnpointRole::Turnpoint),
            create_turnpoint("ESS", 10.0, 0.0, 1.0, TurnpointRole::EssAndGoal),
        ];

        // Flight completes the task twice, with two SSS tags and a single ESS/Goal.
        let mut flight = Flight::default();
        flight.positions = vec![
            create_fix(0.0, 0.0, 0, 0, 0),
            create_fix(1.5, 0.0, 1, 0, 0),   // Tag SSS #1
            create_fix(5.0, 0.0, 2, 0, 0),   // Tag TP1 #1
            create_fix(2.0, 0.0, 2, 30, 0),  // Exit TP1
            create_fix(0.5, 0.0, 2, 45, 0),  // Enter SSS
            create_fix(1.5, 0.0, 3, 0, 0),   // Tag SSS #2
            create_fix(10.0, 0.0, 4, 0, 0),  // Tag ESS/Goal #1
            create_fix(7.5, 0.0, 4, 30, 0),  // Exit ESS/Goal
            create_fix(5.0, 0.0, 5, 20, 0),  // Tag TP1 #2
            create_fix(10.0, 0.0, 6, 30, 0), // Tag ESS/Goal #2
        ];

        let result = score_flight_on_task_detailed(&task, &flight);

        match result {
            Ok(DetailedTaskResult::Completed(completion)) => {
                assert_eq!(completion.attempts.len(), 2);
                assert_eq!(completion.attempts[0].duration, TimeDelta::hours(3));
                assert_eq!(
                    completion.attempts[1].duration,
                    TimeDelta::hours(3).add(TimeDelta::minutes(30))
                );
            }
            _ => {
                panic!("Expected Completed result");
            }
        }
    }
}
