use std::{fmt, fs, io::Error, path::Path, sync::LazyLock, vec::Vec};

use chrono::NaiveTime;
use geographiclib_rs::{Geodesic, InverseGeodesic};
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::{error::Error as JsonError, Value};
use thiserror::Error;
#[cfg(feature = "bindings")]
use ts_rs::TS;

use crate::{
    geo::{Degrees, FloatMeters},
    opti::{
        find_shortest_route, CompactTask, CompactTurnpoint, OptimizationError,
        TaskOptimizationResult,
    },
    turnpoint::{Turnpoint, TurnpointRole},
};

#[derive(Error, Debug)]
pub enum TaskError {
    #[error("Could not read XCTrack file: {0}")]
    Io(#[from] Error),

    #[error("Could not parse XCTrack file: {0}")]
    Format(#[from] JsonError),

    #[error("Could not find task version")]
    MissingVersion,

    #[error("Unknown version: {0}")]
    UnknownVersion(String),

    #[error("Duplicate takeoff turnpoints")]
    DuplicateTakeoff,

    #[error("Invalid takeoff turnpoint position: {0}")]
    InvalidTakeoffPosition(usize),

    #[error("Missing start of speed section")]
    MissingStartOfSpeedSection,

    #[error("Duplicate start of speed section")]
    DuplicateStartOfSpeedSection,

    #[error("Missing end of speed section")]
    MissingEndOfSpeedSection,

    #[error("Duplicate end of speed section")]
    DuplicateEndOfSpeedSection,

    #[error("Invalid goal type: {0}")]
    InvalidGoal(TurnpointRole),

    #[error("Invalid speed section order (SSS at turnpoint index {0} appears after ESS at turnpoint index {1})")]
    InvalidSpeedSectionOrder(usize, usize),

    #[error("Cound not find goal line azimuth")]
    UndefinedGoalLineAzimuth,
}

#[derive(Serialize, Default, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct Time(String);

impl<'de> Deserialize<'de> for Time {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        if NaiveTime::parse_from_str(&s, "%H:%M:%SZ").is_err() {
            return Err(de::Error::custom(format!("Invalid time: {s}")));
        }
        Ok(Time(s))
    }
}

#[derive(Serialize, Deserialize, Default, Debug)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub enum TaskKind {
    #[serde(rename = "CLASSIC")]
    #[default]
    Classic,
}

#[derive(Serialize, Deserialize, Default, Debug)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub enum EarthModel {
    #[serde(rename = "WGS84")]
    #[default]
    Wgs84,
    #[serde(rename = "FAI_SPHERE")]
    FaiSphere,
    #[serde(skip)]
    Cartesian,
}

impl fmt::Display for EarthModel {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            EarthModel::Wgs84 => write!(f, "WGS84"),
            EarthModel::FaiSphere => write!(f, "FAI_SPHERE"),
            EarthModel::Cartesian => write!(f, "CARTESIAN"),
        }
    }
}

impl EarthModel {
    pub fn get_geodesic(&self) -> Geodesic {
        match self {
            EarthModel::Wgs84 => *GEO_WGS84,
            EarthModel::FaiSphere => *GEO_FAI_SPHERE,
            EarthModel::Cartesian => {
                panic!("Cannot get geodesic for cartesian earth model")
            }
        }
    }

    pub fn distance(
        &self,
        lat1: Degrees,
        lon1: Degrees,
        lat2: Degrees,
        lon2: Degrees,
    ) -> FloatMeters {
        match self {
            EarthModel::Wgs84 => wgs84_distance(lat1, lon1, lat2, lon2),
            EarthModel::FaiSphere => fai_sphere_distance(lat1, lon1, lat2, lon2),
            EarthModel::Cartesian => cartesian_distance(lat1, lon1, lat2, lon2),
        }
    }
    pub fn heading(&self, lat1: Degrees, lon1: Degrees, lat2: Degrees, lon2: Degrees) -> Degrees {
        match self {
            EarthModel::Wgs84 => wgs84_heading(lat1, lon1, lat2, lon2),
            EarthModel::FaiSphere => fai_sphere_heading(lat1, lon1, lat2, lon2),
            EarthModel::Cartesian => cartesian_heading(lat1, lon1, lat2, lon2),
        }
    }
}

static GEO_WGS84: LazyLock<Geodesic> = LazyLock::new(Geodesic::wgs84);
static GEO_FAI_SPHERE: LazyLock<Geodesic> = LazyLock::new(|| Geodesic::new(6371e3, 0.0));

fn wgs84_distance(lat1: Degrees, lon1: Degrees, lat2: Degrees, lon2: Degrees) -> FloatMeters {
    GEO_WGS84.inverse(lat1, lon1, lat2, lon2)
}

fn wgs84_heading(lat1: Degrees, lon1: Degrees, lat2: Degrees, lon2: Degrees) -> Degrees {
    let (_, heading, _) = GEO_WGS84.inverse(lat1, lon1, lat2, lon2);
    heading
}

fn fai_sphere_distance(lat1: Degrees, lon1: Degrees, lat2: Degrees, lon2: Degrees) -> FloatMeters {
    GEO_FAI_SPHERE.inverse(lat1, lon1, lat2, lon2)
}

fn fai_sphere_heading(lat1: Degrees, lon1: Degrees, lat2: Degrees, lon2: Degrees) -> Degrees {
    let (_, heading, _) = GEO_FAI_SPHERE.inverse(lat1, lon1, lat2, lon2);
    heading
}

fn cartesian_distance(lat1: Degrees, lon1: Degrees, lat2: Degrees, lon2: Degrees) -> FloatMeters {
    ((lat2 - lat1).powi(2) + (lon2 - lon1).powi(2)).sqrt()
}

fn cartesian_heading(lat1: Degrees, lon1: Degrees, lat2: Degrees, lon2: Degrees) -> Degrees {
    let mut heading = (lon2 - lon1).atan2(lat2 - lat1).to_degrees();
    if heading < 0. {
        heading += 360.;
    }
    heading
}

#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub enum GoalKind {
    #[serde(rename = "CYLINDER")]
    #[default]
    Cylinder,
    #[serde(rename = "LINE")]
    Line,
}

#[derive(Serialize, Deserialize, Default, Debug)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export, optional_fields))]
pub struct Goal {
    #[serde(rename = "type")]
    pub kind: Option<GoalKind>,
    pub deadline: Option<Time>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub enum StartKind {
    #[serde(rename = "RACE")]
    Race,
    #[serde(rename = "ELAPSED-TIME")]
    ElapsedTime,
}

#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub enum StartDirection {
    #[serde(rename = "ENTER")]
    Enter,
    #[serde(rename = "EXIT")]
    Exit,
}

#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct SpeedSectionStart {
    #[serde(rename = "type")]
    pub kind: StartKind,
    pub direction: StartDirection,
    #[serde(rename = "timeGates")]
    pub timegates: Vec<Time>,
}

#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export, optional_fields))]
pub struct Task {
    #[serde(rename = "taskType")]
    pub kind: TaskKind,
    pub version: u8,
    #[serde(rename = "earthModel", default)]
    pub model: EarthModel,
    pub turnpoints: Vec<Turnpoint>,
    pub sss: Option<SpeedSectionStart>,
    pub goal: Option<Goal>,
}

impl fmt::Display for Task {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut result: fmt::Result = writeln!(f, "Task [{}]", &self.model);
        for tp in &self.turnpoints {
            result = result.and(writeln!(
                f,
                "{} - {} ({})",
                tp.waypoint.name, tp.radius, tp.role
            ));
        }
        Ok(())
    }
}

impl Default for Task {
    fn default() -> Self {
        Self::new()
    }
}

impl Task {
    pub fn new() -> Self {
        Self {
            version: 1,
            kind: TaskKind::default(),
            model: EarthModel::default(),
            goal: Some(Goal::default()),
            sss: None,
            turnpoints: vec![],
        }
    }

    pub fn from_xctsk_file(file: &Path) -> Result<Self, TaskError> {
        let contents = fs::read_to_string(file)?;
        Self::from_xctsk_contents(&contents)
    }

    pub fn from_xctsk_contents(contents: &str) -> Result<Self, TaskError> {
        let json: Value = serde_json::from_str(contents)?;
        if json["version"].is_null() {
            return Err(TaskError::MissingVersion);
        }
        if json["version"] != 1 {
            return Err(TaskError::UnknownVersion(json["version"].to_string()));
        }

        let mut task: Self = serde_json::from_value(json)?;
        task.set_goal_turnpoint()?;
        Ok(task)
    }

    pub fn set_goal_turnpoint(&mut self) -> Result<(), TaskError> {
        if let Some(tp) = self.turnpoints.last_mut() {
            match tp.role {
                TurnpointRole::Takeoff | TurnpointRole::Sss => {
                    return Err(TaskError::InvalidGoal(tp.role.clone()));
                }
                TurnpointRole::Ess => tp.role = TurnpointRole::EssAndGoal,
                _ => tp.role = TurnpointRole::Goal,
            }
        }
        Ok(())
    }

    pub fn validate(&self) -> Result<(), TaskError> {
        self.validate_takeoff_position()?;
        self.validate_sss()?;
        self.validate_ess()?;
        self.validate_ss_order()?;
        Ok(())
    }

    fn validate_takeoff_position(&self) -> Result<(), TaskError> {
        let takeoff_positions: Vec<usize> = self
            .turnpoints
            .iter()
            .enumerate()
            .filter(|(_, tp)| tp.role == TurnpointRole::Takeoff)
            .map(|(idx, _)| idx)
            .collect();
        if takeoff_positions.len() > 1 {
            return Err(TaskError::DuplicateTakeoff);
        }
        match takeoff_positions.first() {
            None => Ok(()),
            Some(0) => Ok(()),
            Some(&idx) => Err(TaskError::InvalidTakeoffPosition(idx)),
        }
    }

    fn validate_sss(&self) -> Result<(), TaskError> {
        let n_sss = self
            .turnpoints
            .iter()
            .filter(|tp| tp.role == TurnpointRole::Sss)
            .count();

        match n_sss {
            0 => Err(TaskError::MissingStartOfSpeedSection),
            1 => Ok(()),
            _ => Err(TaskError::DuplicateStartOfSpeedSection),
        }
    }

    fn validate_ess(&self) -> Result<(), TaskError> {
        let n_ess = self
            .turnpoints
            .iter()
            .filter(|tp| tp.role == TurnpointRole::Ess || tp.role == TurnpointRole::EssAndGoal)
            .count();

        match n_ess {
            0 => Err(TaskError::MissingEndOfSpeedSection),
            1 => Ok(()),
            _ => Err(TaskError::DuplicateEndOfSpeedSection),
        }
    }

    fn validate_ss_order(&self) -> Result<(), TaskError> {
        // This is called after checking that the task contains exactly one SSS and one
        // ESS, so we can use position() safely.
        let sss_idx = self
            .turnpoints
            .iter()
            .position(|tp| tp.role == TurnpointRole::Sss);
        let ess_idx = self
            .turnpoints
            .iter()
            .position(|tp| tp.role == TurnpointRole::Ess || tp.role == TurnpointRole::EssAndGoal);

        match (sss_idx, ess_idx) {
            (Some(sss_idx), Some(ess_idx)) => {
                if sss_idx > ess_idx {
                    return Err(TaskError::InvalidSpeedSectionOrder(sss_idx, ess_idx));
                }
                Ok(())
            }
            _ => Ok(()),
        }
    }

    pub fn find_goal_azimuth(&self) -> Result<Degrees, TaskError> {
        let goal = self
            .turnpoints
            .iter()
            .nth_back(0)
            .ok_or(TaskError::UndefinedGoalLineAzimuth)?;
        let previous = self
            .turnpoints
            .iter()
            .rfind(|tp| {
                tp.waypoint.lat != goal.waypoint.lat || tp.waypoint.lon != goal.waypoint.lon
            })
            .ok_or(TaskError::UndefinedGoalLineAzimuth)?;

        let heading = self.model.heading(
            previous.waypoint.lat,
            previous.waypoint.lon,
            goal.waypoint.lat,
            goal.waypoint.lon,
        );
        Ok(heading)
    }

    pub fn find_shortest_route(&self) -> Result<TaskOptimizationResult, OptimizationError> {
        // The takeoff position is excluded from the route optimization
        // computation, as this kind of turnpoint is optional and not used for
        // navigation.
        let sss_idx = self
            .turnpoints
            .iter()
            .position(|tp| tp.role == TurnpointRole::Sss)
            .ok_or(OptimizationError::InvalidTask(
                "Missing start of speed section".into(),
            ))?;
        let ess_idx = self
            .turnpoints
            .iter()
            .position(|tp| tp.role == TurnpointRole::Ess || tp.role == TurnpointRole::EssAndGoal)
            .ok_or(OptimizationError::InvalidTask(
                "Missing end of speed section".into(),
            ))?;

        let n_tries = 10;

        // Speed-section
        let speed_section = find_shortest_route(self.create_subtask(sss_idx, ess_idx), n_tries)?;

        // Pre speed-section
        // Use first point of speed-section as target point to get piecewise continuous optimized route.
        let pre_speed_section = if sss_idx > 0 {
            let mut subtask = self.create_subtask(0, sss_idx - 1);
            let joint = speed_section
                .points
                .first()
                .ok_or(OptimizationError::InvalidTask(
                    "Could not find first turnpoint of speed section".into(),
                ))?;
            subtask.turnpoints.push(CompactTurnpoint {
                lat: joint.lat,
                lon: joint.lon,
                radius: 0.0,
            });
            Some(find_shortest_route(subtask, n_tries)?)
        } else {
            None
        };

        // Post speed-section
        // Use last point of speed-section as starting point to get piecewise continuous optimized route.
        let post_speed_section = if ess_idx < self.turnpoints.len() - 1 {
            let mut subtask = self.create_subtask(ess_idx + 1, self.turnpoints.len());
            let joint = speed_section
                .points
                .last()
                .ok_or(OptimizationError::InvalidTask(
                    "Could not find last turnpoint of speed section".into(),
                ))?;
            subtask.turnpoints.insert(
                0,
                CompactTurnpoint {
                    lat: joint.lat,
                    lon: joint.lon,
                    radius: 0.0,
                },
            );
            Some(find_shortest_route(subtask, n_tries)?)
        } else {
            None
        };

        Ok(TaskOptimizationResult {
            pre_speed_section,
            speed_section,
            post_speed_section,
        })
    }

    fn create_subtask(&self, idx_start: usize, idx_end: usize) -> CompactTask {
        CompactTask {
            geodesic: self.model.get_geodesic(),
            turnpoints: self
                .turnpoints
                .iter()
                .skip(idx_start)
                .take(idx_end - idx_start + 1)
                .map(|tp| CompactTurnpoint {
                    lat: tp.waypoint.lat,
                    lon: tp.waypoint.lon,
                    radius: if self
                        .goal
                        .as_ref()
                        .is_some_and(|g| g.kind == Some(GoalKind::Line))
                        && (tp.role == TurnpointRole::Goal || tp.role == TurnpointRole::EssAndGoal)
                    {
                        0.0
                    } else {
                        tp.radius
                    },
                })
                .collect(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::turnpoint::Waypoint;

    #[test]
    fn test_no_version() {
        let contents = r#"{}"#;
        let result = Task::from_xctsk_contents(contents);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Could not find task version".to_string()
        );
    }

    #[test]
    fn test_unknown_version() {
        let contents = r#"{"version": -1}"#;
        let result = Task::from_xctsk_contents(contents);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Unknown version: -1".to_string()
        );
    }

    #[test]
    fn test_no_takeoff() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "type": "ESS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B02",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "G01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_ok());
    }

    #[test]
    fn test_floating_point_numbers() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "type": "SSS",
                    "radius": 400.0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0.0,
                        "lon": 0.0,
                        "altSmoothed": 100.0
                    }
                },
                {
                    "type": "ESS",
                    "radius": 1000.0,
                    "waypoint": {
                        "name": "B02",
                        "lat": 1.0,
                        "lon": 1.0,
                        "altSmoothed": 2000.0
                    }
                },
                {
                    "radius": 100.0,
                    "waypoint": {
                        "name": "G01",
                        "lat": -1.0,
                        "lon": -1.0,
                        "altSmoothed": 10.0
                    }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_ok());
    }

    #[test]
    fn test_invalid_takeoff_position() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "type": "TAKEOFF",
                    "radius": 0,
                    "waypoint": {
                        "name": "D01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "G01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }
        "#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Invalid takeoff turnpoint position: 1".to_string()
        );
    }

    #[test]
    fn test_duplicate_takeoff() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "type": "TAKEOFF",
                    "radius": 0,
                    "waypoint": {
                        "name": "D01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "type": "TAKEOFF",
                    "radius": 0,
                    "waypoint": {
                        "name": "D02",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "B02",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }
        "#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Duplicate takeoff turnpoints".to_string()
        );
    }

    #[test]
    fn test_no_sss() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "G01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Missing start of speed section".to_string()
        );
    }

    #[test]
    fn test_duplicate_sss() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B02",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "G01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Duplicate start of speed section".to_string()
        );
    }

    #[test]
    fn test_no_ess() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "G01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Missing end of speed section".to_string()
        );
    }

    #[test]
    fn test_duplicate_ess() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "type": "ESS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B02",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "type": "ESS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B03",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "G01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Duplicate end of speed section".to_string()
        );
    }

    #[test]
    fn test_no_goal() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "type": "ESS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B02",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }
        "#;
        let result = Task::from_xctsk_contents(contents);
        assert!(result.is_ok());
        assert_eq!(
            result.unwrap().turnpoints.last().unwrap().role,
            TurnpointRole::EssAndGoal,
        );
    }

    #[test]
    fn test_invalid_ss_order() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "turnpoints": [
                {
                    "type": "ESS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": {
                        "name": "B02",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                },
                {
                    "radius": 0,
                    "waypoint": {
                        "name": "G01",
                        "lat": 0,
                        "lon": 0,
                        "altSmoothed": 0
                    }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents).unwrap().validate();
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Invalid speed section order (SSS at turnpoint index 1 appears after ESS at turnpoint index 0)".to_string()
        );
    }

    #[test]
    fn test_read_xctsk() {
        let contents = include_str!("../data/2024-pwca-monroe-t3.xctsk");
        let task = Task::from_xctsk_contents(contents).unwrap();
        assert_eq!(task.turnpoints.len(), 8);

        let turnpoint_roles: Vec<_> = task.turnpoints.into_iter().map(|t| t.role).collect();
        assert_eq!(
            turnpoint_roles,
            vec![
                TurnpointRole::Takeoff,
                TurnpointRole::Sss,
                TurnpointRole::Turnpoint,
                TurnpointRole::Turnpoint,
                TurnpointRole::Turnpoint,
                TurnpointRole::Turnpoint,
                TurnpointRole::Ess,
                TurnpointRole::Goal
            ]
        );
    }

    #[test]
    pub fn test_round_trip() {
        let contents = include_str!("../data/2024-pwca-monroe-t3.xctsk");
        let task = Task::from_xctsk_contents(contents).unwrap();
        let serialized = serde_json::to_string(&task).unwrap();
        assert_eq!(
            contents.parse::<serde_json::Value>().unwrap(),
            serialized.parse::<serde_json::Value>().unwrap()
        );
    }

    fn create_turnpoint(lat: f64, lon: f64) -> Turnpoint {
        Turnpoint {
            waypoint: Waypoint {
                lat,
                lon,
                alt: 0.0,
                name: String::new(),
                description: None,
            },
            radius: 1000.0,
            role: TurnpointRole::Turnpoint,
        }
    }

    #[test]
    fn line_azimuth_two_turnpoints() {
        let mut task = Task::new();
        task.turnpoints = vec![create_turnpoint(0.0, 0.0), create_turnpoint(0.0, 1.0)];
        let expected_azimuth = 90.0;
        assert_eq!(task.find_goal_azimuth().unwrap(), expected_azimuth);
    }

    #[test]
    fn line_azimuth_two_turnpoints_cartesian() {
        let mut task = Task::new();
        task.model = EarthModel::Cartesian;
        task.turnpoints = vec![create_turnpoint(0.0, 0.0), create_turnpoint(1.0, 0.0)];
        let expected_azimuth = 0.0;
        assert_eq!(task.find_goal_azimuth().unwrap(), expected_azimuth);
    }

    #[test]
    fn line_azimuth_three_turnpoints() {
        let mut task = Task::new();
        task.turnpoints = vec![
            create_turnpoint(0.0, 0.0),
            create_turnpoint(1.0, 0.0),
            create_turnpoint(1.0, 0.0),
        ];
        let expected_azimuth = 0.0;
        assert_eq!(task.find_goal_azimuth().unwrap(), expected_azimuth);
    }

    #[test]
    fn line_azimuth_no_turnpoints() {
        let task = Task::new();
        assert!(matches!(
            task.find_goal_azimuth(),
            Err(TaskError::UndefinedGoalLineAzimuth)
        ));
    }

    #[test]
    fn line_azimuth_single_turnpoint() {
        let mut task = Task::new();
        task.turnpoints = vec![create_turnpoint(0.0, 0.0)];
        assert!(matches!(
            task.find_goal_azimuth(),
            Err(TaskError::UndefinedGoalLineAzimuth)
        ));
    }

    #[test]
    fn line_azimuth_aligned_turnpoints() {
        let mut task = Task::new();
        task.turnpoints = vec![create_turnpoint(0.0, 0.0), create_turnpoint(0.0, 0.0)];
        assert!(matches!(
            task.find_goal_azimuth(),
            Err(TaskError::UndefinedGoalLineAzimuth)
        ));
    }

    #[test]
    fn test_invalid_deadline_format() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "goal": {
                "type": "CYLINDER",
                "deadline": "25:00:00Z"
            },
            "turnpoints": [
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": { "name": "B01", "lat": 0, "lon": 0, "altSmoothed": 0 }
                },
                {
                    "type": "ESS",
                    "radius": 0,
                    "waypoint": { "name": "B02", "lat": 0, "lon": 0, "altSmoothed": 0 }
                },
                {
                    "radius": 0,
                    "waypoint": { "name": "G01", "lat": 0, "lon": 0, "altSmoothed": 0 }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Could not parse XCTrack file: Invalid time: 25:00:00Z".to_string()
        );
    }

    #[test]
    fn test_invalid_timegate_format() {
        let contents = r#"
        {
            "version": 1,
            "taskType": "CLASSIC",
            "sss": {
                "type": "RACE",
                "direction": "ENTER",
                "timeGates": ["12:00:00:00Z"]
            },
            "turnpoints": [
                {
                    "type": "SSS",
                    "radius": 0,
                    "waypoint": { "name": "B01", "lat": 0, "lon": 0, "altSmoothed": 0 }
                },
                {
                    "type": "ESS",
                    "radius": 0,
                    "waypoint": { "name": "B02", "lat": 0, "lon": 0, "altSmoothed": 0 }
                },
                {
                    "radius": 0,
                    "waypoint": { "name": "G01", "lat": 0, "lon": 0, "altSmoothed": 0 }
                }
            ]
        }"#;
        let result = Task::from_xctsk_contents(contents);
        assert!(result.is_err());
        assert_eq!(
            result.unwrap_err().to_string(),
            "Could not parse XCTrack file: Invalid time: 12:00:00:00Z".to_string()
        );
    }
}
