use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use serde::de::{self, Deserializer};

/// Custom deserializer that accepts both string and number for f64
fn deserialize_f64_from_string_or_number<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum StringOrNumber {
        String(String),
        Number(f64),
    }

    match Option::<StringOrNumber>::deserialize(deserializer)? {
        None => Ok(None),
        Some(StringOrNumber::String(s)) => {
            s.parse::<f64>()
                .map(Some)
                .map_err(de::Error::custom)
        }
        Some(StringOrNumber::Number(n)) => Ok(Some(n)),
    }
}

/// GTFS (General Transit Feed Specification) related types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertCondensed {
    pub effect: String,
    pub cause: String,
    pub header: Option<String>,
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub route_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GtfsCondensed {
    pub route: String,
    pub dest: String,
    pub dir: String,
    pub mode: GtfsMode,
    pub times: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub times_live: Option<Vec<Option<String>>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alerts: Option<Vec<AlertCondensed>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GtfsMode {
    LightRail,
    Subway,
    Rail,
    Bus,
    Ferry,
    CableCar,
    Gondola,
    Funicular,
    Trolleybus,
    Monorail,
    Unknown,
}

/// GBFS (General Bikeshare Feed Specification) related types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GbfsCondensed {
    pub name: String,
    pub id: String,
    pub avail: i32,
    pub avail_elec: i32,
    pub avail_std: i32,
    pub docks_avail: i32,
    pub docks_disabled: i32,
    pub capacity: i32,
    pub ebikes_info: Vec<EbikeInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EbikeInfo {
    pub name: String,
    pub battery_pct: f64,
    pub range_mi_cons: f64,
    pub range_me_est: f64,
}
/// Tidal data types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TidalCondensed {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub first_h: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(deserialize_with = "deserialize_f64_from_string_or_number")]
    pub first_hv: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub second_h: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(deserialize_with = "deserialize_f64_from_string_or_number")]
    pub second_hv: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub first_l: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(deserialize_with = "deserialize_f64_from_string_or_number")]
    pub first_lv: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub second_l: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(deserialize_with = "deserialize_f64_from_string_or_number")]
    pub second_lv: Option<f64>,
}

/// Weather data types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeatherCondensed {
    pub name: String,
    pub weather: String,
    pub temp: f64,
    pub feel: f64,
    pub hum: f64,
    pub pressure: f64,
    pub wind: WindInfo,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub visibility: Option<i32>,
    pub units: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindInfo {
    pub speed: f64,
    pub deg: f64,
}

/// Air Quality Index (AQI) data types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AqiCondensed {
    pub name: Option<String>,
    // Additional fields would be added based on HourlyObsData.compile_pairs
    // This is a flexible structure that can contain various pollutant measurements
    #[serde(flatten)]
    pub measurements: std::collections::HashMap<String, serde_json::Value>,
}

/// Ephemeris (astronomical) data types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EphemerisCondensed {
    pub name: String,
    #[serde(flatten)]
    pub periods: std::collections::HashMap<String, String>,
}

/// Calendar event types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarCondensed {
    pub date_start: String, // ISO date format
    pub description: String,
}

/// Cronos data type (passthrough)
pub type CronosCondensed = serde_json::Value;

/// Packages data type (passthrough)
pub type PackagesCondensed = serde_json::Value;

/// Const data type (passthrough)
pub type ConstCondensed = serde_json::Value;

/// GitLab CI job (single record). Mirrors the relevant subset of the GitLab
/// /api/v4/projects/{id}/jobs response shape that the apollos-crib worker emits.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitlabCondensed {
    pub id: u64,
    pub status: String,
    pub name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stage: Option<String>,
    #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
    pub git_ref: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub web_url: Option<String>,
    #[serde(default)]
    pub allow_failure: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub commit: Option<GitlabCommit>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub duration: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub queued_duration: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub coverage: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub finished_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub failure_reason: Option<String>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitlabCommit {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub short_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub author_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub author_email: Option<String>,
}

/// GitHub Actions workflow run or job. Both shapes share enough fields that
/// one struct with optionals covers both — in apollos-crib a query targets
/// runs OR jobs (level field), and the worker tags emitted records with the
/// `repository.full_name` so multi-repo namespace queries are self-describing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GithubCondensed {
    pub id: u64,
    /// Lifecycle phase. Runs: queued/in_progress/completed.
    /// Jobs: queued/in_progress/completed/waiting.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    /// Final outcome (only meaningful when status == "completed"):
    /// success/failure/cancelled/skipped/timed_out/neutral/action_required.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub conclusion: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Run-level: human-friendly title (often the head commit message subject).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub display_title: Option<String>,
    /// Run-level: triggering event (push, pull_request, schedule, etc.).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub event: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub head_branch: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub head_sha: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub head_commit: Option<GithubCommit>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub repository: Option<GithubRepoRef>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub actor: Option<GithubActor>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub html_url: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub run_started_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub completed_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub updated_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub run_attempt: Option<u32>,
    /// Job-level: id of the parent run.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub run_id: Option<u64>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GithubCommit {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub author: Option<GithubAuthor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GithubAuthor {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub email: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GithubRepoRef {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub full_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub owner: Option<GithubOwner>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GithubOwner {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub login: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GithubActor {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub login: Option<String>,
}

/// Drought records. The apollos-crib drought source is multi-product; today
/// only USDM has a real fetcher. Other products emit a stub record so consumers
/// can render a "not implemented yet" placeholder.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DroughtCondensed {
    Usdm(UsdmStats),
    Stub(DroughtStub),
}

/// US Drought Monitor county- or state-level area-percentage statistic.
/// `none + d0 + d1 + d2 + d3 + d4` = 100. Higher D-letters mean worse drought.
/// Note `d0` includes worse categories (USDM cumulative reporting), so a county
/// with 100% d0 / 50% d1 / 0% d2 means half the area is at D1 or worse.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsdmStats {
    /// ISO-8601 timestamp of the published map. Required — distinguishes a real
    /// USDM record from the stub variant via the untagged enum.
    #[serde(rename = "mapDate")]
    pub map_date: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fips: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub state: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub county: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub none: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub d0: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub d1: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub d2: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub d3: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub d4: Option<f64>,
    #[serde(rename = "validStart", default, skip_serializing_if = "Option::is_none")]
    pub valid_start: Option<String>,
    #[serde(rename = "validEnd", default, skip_serializing_if = "Option::is_none")]
    pub valid_end: Option<String>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DroughtStub {
    pub product: String,
    pub status: String,
}

/// Pollen daily forecast entry (Google Pollen API /v1/forecast:lookup shape).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollenCondensed {
    pub date: PollenDate,
    #[serde(rename = "pollenTypeInfo", default)]
    pub pollen_type_info: Vec<PollenTypeInfo>,
    #[serde(rename = "plantInfo", default, skip_serializing_if = "Option::is_none")]
    pub plant_info: Option<Vec<PollenPlantInfo>>,
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollenDate {
    pub year: u32,
    pub month: u32,
    pub day: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollenTypeInfo {
    /// "GRASS" | "TREE" | "WEED"
    pub code: String,
    #[serde(rename = "displayName", default, skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,
    #[serde(rename = "inSeason", default)]
    pub in_season: bool,
    #[serde(rename = "indexInfo", default, skip_serializing_if = "Option::is_none")]
    pub index_info: Option<PollenIndexInfo>,
    #[serde(rename = "healthRecommendations", default, skip_serializing_if = "Option::is_none")]
    pub health_recommendations: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollenPlantInfo {
    /// Species code, e.g. "OAK", "RAGWEED", "TIMOTHY".
    pub code: String,
    #[serde(rename = "displayName", default, skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,
    #[serde(rename = "inSeason", default)]
    pub in_season: bool,
    #[serde(rename = "indexInfo", default, skip_serializing_if = "Option::is_none")]
    pub index_info: Option<PollenIndexInfo>,
    #[serde(rename = "plantDescription", default, skip_serializing_if = "Option::is_none")]
    pub plant_description: Option<serde_json::Value>,
}

/// Universal Pollen Index value + named category. UPI ranges 0..=5, with 0
/// meaning "None" and 5 meaning "Very High".
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollenIndexInfo {
    /// Always "UPI" today.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub code: Option<String>,
    #[serde(rename = "displayName", default, skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub value: Option<u8>,
    /// "None" | "Very Low" | "Low" | "Moderate" | "High" | "Very High"
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub category: Option<String>,
    #[serde(rename = "indexDescription", default, skip_serializing_if = "Option::is_none")]
    pub index_description: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub color: Option<serde_json::Value>,
}

/// Query information metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryInfo {
    pub name: String,
    #[serde(default)]
    pub meta: HashMap<String, serde_json::Value>,
}

/// Wrapped data with query information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WrappedData {
    pub data: CondensedData,
    pub query: QueryInfo,
}

#[derive(Debug, Clone)]
struct DataEntry {
    content: CondensedData,
    query_info: Option<QueryInfo>,
    meta: HashMap<String, serde_json::Value>,
}

/// Data can be either wrapped or unwrapped for backwards compatibility
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MaybeWrappedData {
    Wrapped(WrappedData),
    Unwrapped(CondensedData),
}

/// Main enum representing all possible condensed data types.
///
/// Note: GitLab/GitHub/Drought/Pollen are emitted as lists since the upstream
/// workers fetch many records per refresh. Cronos/Packages/Const remain
/// passthroughs to `serde_json::Value` because their condense step in
/// apollos-crib doesn't normalize the shape.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CondensedData {
    Gtfs(Vec<GtfsCondensed>),
    Gbfs(Vec<GbfsCondensed>),
    Tidal(Vec<TidalCondensed>),
    Weather(Vec<WeatherCondensed>),
    Aqi(Vec<AqiCondensed>),
    Ephem(Vec<EphemerisCondensed>),
    Calendar(Vec<CalendarCondensed>),
    Cronos(CronosCondensed),
    Gitlab(Vec<GitlabCondensed>),
    Github(Vec<GithubCondensed>),
    Drought(Vec<DroughtCondensed>),
    Pollen(Vec<PollenCondensed>),
    Packages(PackagesCondensed),
    Const(ConstCondensed),
}

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

    #[test]
    fn test_gtfs_serialization() {
        let gtfs = GtfsCondensed {
            route: "1".to_string(),
            dest: "Downtown".to_string(),
            dir: "North".to_string(),
            mode: GtfsMode::Subway,
            times: vec!["10:30".to_string(), "10:45".to_string()],
            times_live: Some(vec![Some("10:32".to_string()), Some("10:46".to_string())]),
            alerts: None,
        };

        let json = serde_json::to_string(&gtfs).unwrap();
        assert!(json.contains("Downtown"));
    }

    #[test]
    fn test_gbfs_serialization() {
        let gbfs = GbfsCondensed {
            name: "Main St Station".to_string(),
            id: "123".to_string(),
            avail: 5,
            avail_elec: 2,
            avail_std: 3,
            docks_avail: 10,
            docks_disabled: 1,
            capacity: 15,
            ebikes_info: vec![
                EbikeInfo {
                    name: "E001".to_string(),
                    battery_pct: 85.0,
                    range_mi_cons: 20.0,
                    range_me_est: 25.0,
                }
            ],
        };

        let json = serde_json::to_string(&gbfs).unwrap();
        assert!(json.contains("Main St Station"));
    }

    #[test]
    fn test_weather_serialization() {
        let weather = WeatherCondensed {
            name: "San Francisco".to_string(),
            weather: "Clear".to_string(),
            temp: 72.5,
            feel: 70.0,
            hum: 65.0,
            pressure: 1013.25,
            wind: WindInfo {
                speed: 5.5,
                deg: 180.0,
            },
            visibility: Some(10000),
            units: "imperial".to_string(),
        };

        let json = serde_json::to_string(&weather).unwrap();
        assert!(json.contains("San Francisco"));
    }

    #[test]
    fn test_gitlab_roundtrip() {
        let raw = r#"{
            "id": 12345,
            "status": "running",
            "name": "rspec",
            "stage": "test",
            "ref": "main",
            "web_url": "https://gitlab.example/group/proj/-/jobs/12345",
            "allow_failure": false,
            "duration": 42.5,
            "commit": {
                "id": "abcdef0123456789",
                "short_id": "abcdef01",
                "title": "Fix the thing",
                "author_name": "Alice"
            }
        }"#;
        let parsed: GitlabCondensed = serde_json::from_str(raw).unwrap();
        assert_eq!(parsed.id, 12345);
        assert_eq!(parsed.status, "running");
        assert_eq!(parsed.git_ref.as_deref(), Some("main"));
        assert_eq!(parsed.commit.unwrap().short_id.unwrap(), "abcdef01");
    }

    #[test]
    fn test_github_roundtrip_run() {
        let raw = r#"{
            "id": 999,
            "status": "completed",
            "conclusion": "success",
            "name": "ci.yml",
            "display_title": "Fix CI",
            "event": "push",
            "head_branch": "main",
            "head_sha": "deadbeef",
            "repository": {"full_name": "octo/Hello-World", "owner": {"login": "octo"}},
            "html_url": "https://github.com/octo/Hello-World/actions/runs/999",
            "run_started_at": "2026-05-10T12:00:00Z",
            "run_attempt": 1
        }"#;
        let parsed: GithubCondensed = serde_json::from_str(raw).unwrap();
        assert_eq!(parsed.id, 999);
        assert_eq!(parsed.conclusion.as_deref(), Some("success"));
        assert_eq!(
            parsed.repository.unwrap().full_name.as_deref(),
            Some("octo/Hello-World")
        );
    }

    #[test]
    fn test_drought_usdm_roundtrip() {
        let raw = r#"{
            "mapDate": "2026-05-05T00:00:00",
            "fips": "25025",
            "county": "Suffolk County",
            "state": "MA",
            "none": 0.0,
            "d0": 100.0,
            "d1": 97.75,
            "d2": 0.0,
            "d3": 0.0,
            "d4": 0.0,
            "validStart": "2026-05-05T00:00:00",
            "validEnd": "2026-05-11T23:59:59",
            "statisticFormatID": 1
        }"#;
        let parsed: DroughtCondensed = serde_json::from_str(raw).unwrap();
        match parsed {
            DroughtCondensed::Usdm(s) => {
                assert_eq!(s.fips.as_deref(), Some("25025"));
                assert_eq!(s.d1, Some(97.75));
                assert!(s.map_date.starts_with("2026-05-05"));
            }
            _ => panic!("expected Usdm variant"),
        }
    }

    #[test]
    fn test_drought_stub_roundtrip() {
        let raw = r#"{"product": "csi", "status": "not_yet_implemented"}"#;
        let parsed: DroughtCondensed = serde_json::from_str(raw).unwrap();
        match parsed {
            DroughtCondensed::Stub(s) => {
                assert_eq!(s.product, "csi");
                assert_eq!(s.status, "not_yet_implemented");
            }
            _ => panic!("expected Stub variant"),
        }
    }

    #[test]
    fn test_pollen_roundtrip() {
        let raw = r#"{
            "date": {"year": 2026, "month": 5, "day": 10},
            "pollenTypeInfo": [
                {
                    "code": "GRASS",
                    "displayName": "Grass",
                    "inSeason": true,
                    "indexInfo": {
                        "code": "UPI",
                        "value": 3,
                        "category": "Moderate"
                    }
                }
            ],
            "plantInfo": [
                {
                    "code": "OAK",
                    "displayName": "Oak",
                    "inSeason": true,
                    "indexInfo": {"code": "UPI", "value": 4, "category": "High"}
                }
            ]
        }"#;
        let parsed: PollenCondensed = serde_json::from_str(raw).unwrap();
        assert_eq!(parsed.date.year, 2026);
        assert_eq!(parsed.pollen_type_info.len(), 1);
        let grass = &parsed.pollen_type_info[0];
        assert_eq!(grass.code, "GRASS");
        assert_eq!(grass.index_info.as_ref().unwrap().value, Some(3));
        assert_eq!(parsed.plant_info.unwrap()[0].code, "OAK");
    }
}
