// NOTE: these tests are meant for the cities15000 dataset
use serial_test::serial;

use crate::workdir::Workdir;

#[test]
fn geocode_suggest() {
    let wrk = Workdir::new("geocode_suggest");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Melrose, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["Brooklyn, New York"],
            svec!["East Harlem, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["Jersey City, New Jersey"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest").arg("Location").arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["(41.90059, -87.85673)"],
        svec!["(28.11085, -82.69482)"],
        svec!["(40.71427, -74.00597)"],
        svec!["(45.09413, -93.35634)"],
        svec!["(41.27621, -72.86843)"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["(40.72816, -74.07764)"],
        svec!["95.213424, 190,1234565"], // suggest expects a city name, not lat, long
        svec!["(14.55027, 121.03269)"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_select() {
    let wrk = Workdir::new("geocode_suggest_select");
    wrk.create(
        "data.csv",
        vec![
            svec!["c1", "c2", "Location"],
            svec!["1", "2", "Melrose, New York"],
            svec!["3", "4", "East Flatbush, New York"],
            svec!["5", "6", "Manhattan, New York"],
            svec!["7", "8", "Brooklyn, New York"],
            svec!["9", "10", "East Harlem, New York"],
            svec![
                "11",
                "12",
                "This is not a Location and it will not be geocoded"
            ],
            svec!["13", "14", "Jersey City, New Jersey"],
            svec!["15", "16", "95.213424, 190,1234565"], // invalid lat, long
            svec!["17", "18", "Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    // use select syntax to select the last column
    cmd.arg("suggest").arg("_").arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["c1", "c2", "Location"],
        svec!["1", "2", "(41.90059, -87.85673)"],
        svec!["3", "4", "(28.11085, -82.69482)"],
        svec!["5", "6", "(40.71427, -74.00597)"],
        svec!["7", "8", "(45.09413, -93.35634)"],
        svec!["9", "10", "(41.27621, -72.86843)"],
        svec![
            "11",
            "12",
            "This is not a Location and it will not be geocoded"
        ],
        svec!["13", "14", "(40.72816, -74.07764)"],
        svec!["15", "16", "95.213424, 190,1234565"], // suggest expects a city name, not lat, long
        svec!["17", "18", "(14.55027, 121.03269)"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggestnow_default() {
    let wrk = Workdir::new("geocode_suggestnow_default");
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggestnow").arg("Brooklyn");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Brooklyn, New York US: 40.6501, -73.94958"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggestnow_formatstr_dyncols() {
    let wrk = Workdir::new("geocode_suggestnow_formatstr_dyncols");

    let mut cmd = wrk.command("geocode");
    cmd.arg("suggestnow").arg("Secaucus").args([
        "--formatstr",
        "%dyncols: {population:population}, {state:admin1}, {county:admin2}, \
         {state_fips:us_state_fips_code}, {county_fips:us_county_fips_code}, {timezone:timezone}",
    ]);

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec![
            "Location",
            "population",
            "state",
            "county",
            "state_fips",
            "county_fips",
            "timezone",
        ],
        svec![
            "Secaucus",
            "19104",
            "New Jersey",
            "Hudson",
            "34",
            "017",
            "America/New_York"
        ],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_intl() {
    let wrk = Workdir::new("geocode_suggest_intl");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Paris"],
            svec!["Manila"],
            svec!["London"],
            svec!["Berlin"],
            svec!["Moscow"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["Brazil"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Havana"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .args(["-f", "%city-admin1-country"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);
    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Paris, Île-de-France Region FR"],
        svec!["Manila, National Capital Region PH"],
        svec!["London, England GB"],
        svec!["Berlin, State of Berlin DE"],
        svec!["Moscow, Moscow RU"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["Brasília, Federal District BR"],
        svec!["95.213424, 190,1234565"],
        svec!["Havana, La Habana Province CU"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_intl_country_filter() {
    let wrk = Workdir::new("geocode_suggest_intl_country_filter");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Paris"],
            svec!["Manila"],
            svec!["London"],
            svec!["Berlin"],
            svec!["Moscow"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["Brazil"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Havana"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .args(["--country", "US"])
        .args(["-f", "%city-admin1-country"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Paris, Texas US"],
        svec!["Manteca, California US"],
        svec!["Sterling, Virginia US"],
        svec!["Burlington, North Carolina US"],
        svec!["Moscow, Idaho US"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["Brawley, California US"],
        svec!["95.213424, 190,1234565"],
        svec!["Savannah, Georgia US"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_intl_admin1_filter_error() {
    let wrk = Workdir::new("geocode_suggest_intl_admin1_filter_error");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Paris"],
            svec!["Manila"],
            svec!["London"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .args(["--admin1", "US"])
        .args(["-f", "%city-admin1-country"])
        .arg("data.csv");

    // admin1 requires a country filter
    wrk.assert_err(&mut cmd);
}

#[test]
fn geocode_suggestnow() {
    let wrk = Workdir::new("geocode_suggestnow");

    let mut cmd = wrk.command("geocode");
    cmd.arg("suggestnow")
        .arg("Paris")
        .args(["--country", "US"])
        .args(["-f", "%city-admin1-country"]);

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![svec!["Location"], svec!["Paris, Texas US"]];
    assert_eq!(got, expected);
}

#[test]
fn geocode_reversenow() {
    let wrk = Workdir::new("geocode_reversenow");

    let mut cmd = wrk.command("geocode");
    cmd.arg("reversenow").arg("(40.67, -73.94)").args([
        "-f",
        "{name}, {admin2} County, {admin1} - {population} {timezone}",
    ]);

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Brooklyn, Kings County, New York - 2736074 America/New_York"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_reversenow_error() {
    let wrk = Workdir::new("geocode_reversenow_error");

    let mut cmd = wrk.command("geocode");
    cmd.arg("reversenow")
        .arg("(40.67, -73.94)")
        .args(["--admin1", "New York"])
        .args([
            "-f",
            "{name}, {admin2} County, {admin1} - {population} {timezone}",
        ]);

    // reversenow does not support admin1 filter
    wrk.assert_err(&mut cmd);
}

#[test]
fn geocode_suggest_intl_admin1_filter_country_inferencing() {
    let wrk = Workdir::new("geocode_suggest_intl_admin1_filter_country_inferencing");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Paris"],
            svec!["Manila"],
            svec!["London"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .args(["--admin1", "US.NJ,US.TX,US.NY"])
        .args(["-f", "%city-admin1-country"])
        .arg("data.csv");

    // admin1 requires a country filter
    // however, since all the admin1 filters are in the US
    // or more specifically, the admin1 filters have a prefix of "US."
    // the country filter is inferred to be "US"
    wrk.assert_success(&mut cmd);
}

#[test]
fn geocode_suggest_intl_multi_country_filter() {
    let wrk = Workdir::new("geocode_suggest_intl_multi_country_filter");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Paris"],
            svec!["Manila"],
            svec!["London"],
            svec!["Berlin"],
            svec!["Moscow"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["Brazil"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Havana"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .args(["--country", "us,FR,ru"])
        .args(["-f", "%city-admin1-country"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Paris, Île-de-France Region FR"],
        svec!["Manteca, California US"],
        svec!["Sterling, Virginia US"],
        svec!["Burlington, North Carolina US"],
        svec!["Moscow, Moscow RU"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["Brawley, California US"],
        svec!["95.213424, 190,1234565"],
        svec!["Savannah, Georgia US"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_filter_country_admin1() {
    let wrk = Workdir::new("geocode_suggest_filter_country_admin1");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Melrose, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["Brooklyn, New York"],
            svec!["East Harlem, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["Jersey City, New Jersey"],
            svec!["(41.90059, -87.85673)"],
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .args(["-f", "{name}, {admin1}, {admin2} {country}"])
        .args(["--country", "US"])
        .args(["--admin1", "US.NY,New J,Metro Manila"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Melrose Park, Illinois, Cook US"],
        svec!["Elmwood Park, New Jersey, Bergen County US"],
        svec!["New York, New York,  US"],
        svec!["Brooklyn, New York, Kings US"],
        svec!["East Haven, Connecticut,  US"],
        svec!["This is not a Location and it will not be geocoded"],
        // Jersey City matched as the admin1 filter included "New J"
        // which starts_with match "New Jersey"
        svec!["Jersey City, New Jersey, Hudson US"],
        // suggest expects a city name, not lat, long
        svec!["(41.90059, -87.85673)"],
        // Makati did not match, even with the Metro Manila admin1 filter
        // as the country filter was set to US
        // as a result, the country filter takes precedence over the admin1 filter
        // and the closest match for Makati in the US is McAllen in Texas
        svec!["McKinney, Texas, Collin US"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_invalid() {
    let wrk = Workdir::new("geocode_suggest_invalid");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Melrose, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["East Harlem, New York"],
            svec!["Brooklyn, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["Jersey City, New Jersey"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .args(["--invalid-result", "<ERROR>"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["(41.90059, -87.85673)"],
        svec!["(28.11085, -82.69482)"],
        svec!["(40.71427, -74.00597)"],
        svec!["(41.27621, -72.86843)"],
        svec!["(45.09413, -93.35634)"],
        svec!["<ERROR>"],
        svec!["(40.72816, -74.07764)"],
        svec!["<ERROR>"], // suggest expects a city name, not lat, long
        svec!["(14.55027, 121.03269)"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_dynfmt() {
    let wrk = Workdir::new("geocode_suggest_dynfmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Melrose, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["East Harlem, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .arg("--formatstr")
        .arg(
            "{latitude}:{longitude} - {name}, {admin1}:{us_state_fips_code}-{us_county_fips_code} \
             {country} {continent} {currency_code} {neighbours}",
        )
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["41.90059:-87.85673 - Melrose Park, Illinois:17-031 US NA USD CA,MX,CU"],
        svec!["28.11085:-82.69482 - East Lake, Florida:12-003 US NA USD CA,MX,CU"],
        svec!["40.71427:-74.00597 - New York, New York:36- US NA USD CA,MX,CU"],
        svec!["41.27621:-72.86843 - East Haven, Connecticut:09-070 US NA USD CA,MX,CU"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["95.213424, 190,1234565"], // invalid lat, long
        svec!["14.55027:121.03269 - Makati City, National Capital Region:- PH AS PHP "],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_pretty_json() {
    let wrk = Workdir::new("geocode_suggest_pretty_json");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Melrose, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["East Harlem, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .arg("--formatstr")
        .arg("%pretty-json")
        .arg("data.csv");

    wrk.assert_success(&mut cmd);
    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec![
            "{\n  \"cityrecord\":{\n  \"id\": 4901868,\"name\": \"Melrose Park\",\"latitude\": \
             41.90058898925781,\"longitude\": -87.85672760009766,\"country\": {\n    \"id\": \
             6252001,\"code\": \"US\",\"name\": \"United States\"\n  },\"admin_division\": {\n    \
             \"id\": 4896861,\"code\": \"US.IL\",\"name\": \"Illinois\"\n  },\"admin2_division\": \
             {\n    \"id\": 4888671,\"code\": \"US.IL.031\",\"name\": \"Cook County\"\n  \
             },\"timezone\": \"America/Chicago\",\"names\": {\n    \"en\": \"Melrose Park\"\n  \
             },\"country_names\": {\n    \"en\": \"United States\"\n  },\"admin1_names\": {\n    \
             \"en\": \"Illinois\"\n  },\"admin2_names\": {\n    \"en\": \"Cook\"\n  \
             },\"population\": 25379\n},\n  \"countryrecord\":{\n  \"info\": {\n    \"iso\": \
             \"US\",\"iso3\": \"USA\",\"iso_numeric\": \"840\",\"fips\": \"US\",\"name\": \
             \"United States\",\"capital\": \"Washington\",\"area\": \"9629091\",\"population\": \
             327167434,\"continent\": \"NA\",\"tld\": \".us\",\"currency_code\": \
             \"USD\",\"currency_name\": \"Dollar\",\"phone\": \"1\",\"postal_code_format\": \
             \"#####-####\",\"postal_code_regex\": \"^\\\\d{5}(-\\\\d{4})?$\",\"languages\": \
             \"en-US,es-US,haw,fr\",\"geonameid\": 6252001,\"neighbours\": \
             \"CA,MX,CU\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \"United \
             States\"\n  },\"capital_names\": {\n    \"en\": \"Washington D.C.\"\n  }\n},\n  \
             \"us_fips_codes\":{\n  \"us_state_code\": \"IL\",\n  \"us_state_name\": \
             \"Illinois\",\n  \"us_state_fips_code\": \"17\",\n  \"us_county\": \"Cook\",\n  \
             \"us_county_fips_code\": \"031\"\n}\n}"
        ],
        svec![
            "{\n  \"cityrecord\":{\n  \"id\": 4154008,\"name\": \"East Lake\",\"latitude\": \
             28.110849380493164,\"longitude\": -82.69481658935547,\"country\": {\n    \"id\": \
             6252001,\"code\": \"US\",\"name\": \"United States\"\n  },\"admin_division\": {\n    \
             \"id\": 4155751,\"code\": \"US.FL\",\"name\": \"Florida\"\n  },\"admin2_division\": \
             {\n    \"id\": 4168618,\"code\": \"US.FL.103\",\"name\": \"Pinellas County\"\n  \
             },\"timezone\": \"America/New_York\",\"names\": null,\"country_names\": {\n    \
             \"en\": \"United States\"\n  },\"admin1_names\": {\n    \"en\": \"Florida\"\n  \
             },\"admin2_names\": {\n    \"en\": \"Pinellas\"\n  },\"population\": 30962\n},\n  \
             \"countryrecord\":{\n  \"info\": {\n    \"iso\": \"US\",\"iso3\": \
             \"USA\",\"iso_numeric\": \"840\",\"fips\": \"US\",\"name\": \"United \
             States\",\"capital\": \"Washington\",\"area\": \"9629091\",\"population\": \
             327167434,\"continent\": \"NA\",\"tld\": \".us\",\"currency_code\": \
             \"USD\",\"currency_name\": \"Dollar\",\"phone\": \"1\",\"postal_code_format\": \
             \"#####-####\",\"postal_code_regex\": \"^\\\\d{5}(-\\\\d{4})?$\",\"languages\": \
             \"en-US,es-US,haw,fr\",\"geonameid\": 6252001,\"neighbours\": \
             \"CA,MX,CU\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \"United \
             States\"\n  },\"capital_names\": {\n    \"en\": \"Washington D.C.\"\n  }\n},\n  \
             \"us_fips_codes\":{\n  \"us_state_code\": \"FL\",\n  \"us_state_name\": \
             \"Florida\",\n  \"us_state_fips_code\": \"12\",\n  \"us_county\": \"Pinellas\",\n  \
             \"us_county_fips_code\": \"003\"\n}\n}"
        ],
        svec![
            "{\n  \"cityrecord\":{\n  \"id\": 5128581,\"name\": \"New York City\",\"latitude\": \
             40.714271545410156,\"longitude\": -74.00596618652344,\"country\": {\n    \"id\": \
             6252001,\"code\": \"US\",\"name\": \"United States\"\n  },\"admin_division\": {\n    \
             \"id\": 5128638,\"code\": \"US.NY\",\"name\": \"New York\"\n  },\"admin2_division\": \
             null,\"timezone\": \"America/New_York\",\"names\": {\n    \"en\": \"New York\"\n  \
             },\"country_names\": {\n    \"en\": \"United States\"\n  },\"admin1_names\": {\n    \
             \"en\": \"New York\"\n  },\"admin2_names\": null,\"population\": 8804190\n},\n  \
             \"countryrecord\":{\n  \"info\": {\n    \"iso\": \"US\",\"iso3\": \
             \"USA\",\"iso_numeric\": \"840\",\"fips\": \"US\",\"name\": \"United \
             States\",\"capital\": \"Washington\",\"area\": \"9629091\",\"population\": \
             327167434,\"continent\": \"NA\",\"tld\": \".us\",\"currency_code\": \
             \"USD\",\"currency_name\": \"Dollar\",\"phone\": \"1\",\"postal_code_format\": \
             \"#####-####\",\"postal_code_regex\": \"^\\\\d{5}(-\\\\d{4})?$\",\"languages\": \
             \"en-US,es-US,haw,fr\",\"geonameid\": 6252001,\"neighbours\": \
             \"CA,MX,CU\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \"United \
             States\"\n  },\"capital_names\": {\n    \"en\": \"Washington D.C.\"\n  }\n},\n  \
             \"us_fips_codes\":{\n  \"us_state_code\": \"NY\",\n  \"us_state_name\": \"New \
             York\",\n  \"us_state_fips_code\": \"36\",\n  \"us_county\": \"\",\n  \
             \"us_county_fips_code\": null\n}\n}"
        ],
        svec![
            "{\n  \"cityrecord\":{\n  \"id\": 4833425,\"name\": \"East Haven\",\"latitude\": \
             41.27621078491211,\"longitude\": -72.8684310913086,\"country\": {\n    \"id\": \
             6252001,\"code\": \"US\",\"name\": \"United States\"\n  },\"admin_division\": {\n    \
             \"id\": 4831725,\"code\": \"US.CT\",\"name\": \"Connecticut\"\n  \
             },\"admin2_division\": {\n    \"id\": 12809023,\"code\": \"US.CT.170\",\"name\": \
             \"South Central Connecticut Planning Region\"\n  },\"timezone\": \
             \"America/New_York\",\"names\": {\n    \"en\": \"East Haven\"\n  \
             },\"country_names\": {\n    \"en\": \"United States\"\n  },\"admin1_names\": {\n    \
             \"en\": \"Connecticut\"\n  },\"admin2_names\": null,\"population\": 29257\n},\n  \
             \"countryrecord\":{\n  \"info\": {\n    \"iso\": \"US\",\"iso3\": \
             \"USA\",\"iso_numeric\": \"840\",\"fips\": \"US\",\"name\": \"United \
             States\",\"capital\": \"Washington\",\"area\": \"9629091\",\"population\": \
             327167434,\"continent\": \"NA\",\"tld\": \".us\",\"currency_code\": \
             \"USD\",\"currency_name\": \"Dollar\",\"phone\": \"1\",\"postal_code_format\": \
             \"#####-####\",\"postal_code_regex\": \"^\\\\d{5}(-\\\\d{4})?$\",\"languages\": \
             \"en-US,es-US,haw,fr\",\"geonameid\": 6252001,\"neighbours\": \
             \"CA,MX,CU\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \"United \
             States\"\n  },\"capital_names\": {\n    \"en\": \"Washington D.C.\"\n  }\n},\n  \
             \"us_fips_codes\":{\n  \"us_state_code\": \"CT\",\n  \"us_state_name\": \
             \"Connecticut\",\n  \"us_state_fips_code\": \"09\",\n  \"us_county\": \"\",\n  \
             \"us_county_fips_code\": \"070\"\n}\n}"
        ],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["95.213424, 190,1234565"],
        svec![
            "{\n  \"cityrecord\":{\n  \"id\": 1703417,\"name\": \"Makati City\",\"latitude\": \
             14.550270080566406,\"longitude\": 121.0326919555664,\"country\": {\n    \"id\": \
             1694008,\"code\": \"PH\",\"name\": \"Philippines\"\n  },\"admin_division\": {\n    \
             \"id\": 7521311,\"code\": \"PH.NCR\",\"name\": \"National Capital Region\"\n  \
             },\"admin2_division\": {\n    \"id\": 11395838,\"code\": \
             \"PH.NCR.137600000\",\"name\": \"Southern Manila District\"\n  },\"timezone\": \
             \"Asia/Manila\",\"names\": {\n    \"en\": \"Makati City\"\n  },\"country_names\": \
             {\n    \"en\": \"Philippines\"\n  },\"admin1_names\": {\n    \"en\": \"National \
             Capital Region\"\n  },\"admin2_names\": null,\"population\": 510383\n},\n  \
             \"countryrecord\":{\n  \"info\": {\n    \"iso\": \"PH\",\"iso3\": \
             \"PHL\",\"iso_numeric\": \"608\",\"fips\": \"RP\",\"name\": \
             \"Philippines\",\"capital\": \"Manila\",\"area\": \"300000\",\"population\": \
             106651922,\"continent\": \"AS\",\"tld\": \".ph\",\"currency_code\": \
             \"PHP\",\"currency_name\": \"Peso\",\"phone\": \"63\",\"postal_code_format\": \
             \"####\",\"postal_code_regex\": \"^(\\\\d{4})$\",\"languages\": \
             \"tl,en-PH,fil,ceb,ilo,hil,war,pam,bik,bcl,pag,mrw,tsg,mdh,cbk,krj,sgd,msb,akl,ibg,\
             yka,mta,abx\",\"geonameid\": 1694008,\"neighbours\": \"\",\"equivalent_fips_code\": \
             \"\"\n  },\"names\": {\n    \"en\": \"Philippines\"\n  },\"capital_names\": {\n    \
             \"en\": \"Manila\"\n  }\n},\n  \"us_fips_codes\":{\n  \"us_state_code\": null,\n  \
             \"us_state_name\": \"National Capital Region\",\n  \"us_state_fips_code\": null,\n  \
             \"us_county\": \"\",\n  \"us_county_fips_code\": null\n}\n}"
        ],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_invalid_dynfmt() {
    let wrk = Workdir::new("geocode_suggest_invalid_dynfmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Melrose, New York"],
            svec!["East Flatbush, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .arg("--formatstr")
        .arg("{latitude}:{longitude} - {name}, {admin1} {invalid_field}")
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Invalid dynfmt template."],
        svec!["Invalid dynfmt template."],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["95.213424, 190,1234565"], // invalid lat, long
        svec!["Invalid dynfmt template."],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_fmt() {
    let wrk = Workdir::new("geocode_suggest_fmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Elmhurst, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["East Harlem, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["40.71427, -74.00597"],
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .arg("--formatstr")
        .arg("%city-state-country")
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Elmhurst, Illinois US"],
        svec!["East Lake, Florida US"],
        svec!["New York, New York US"],
        svec!["East Haven, Connecticut US"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["40.71427, -74.00597"], // suggest doesn't work with lat, long
        svec!["Makati City, National Capital Region PH"],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_fmt_json() {
    let wrk = Workdir::new("geocode_suggest_fmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Elmhurst, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["East Harlem, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["40.71427, -74.00597"],
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .arg("--formatstr")
        .arg("%json")
        .arg("data.csv");

    let got: String = wrk.stdout(&mut cmd);

    wrk.assert_success(&mut cmd);

    let expected = r######"Location
"{""cityrecord"":{""id"":4891010,""name"":""Elmhurst"",""latitude"":41.899471282958984,""longitude"":-87.94033813476562,""country"":{""id"":6252001,""code"":""US"",""name"":""United States""},""admin_division"":{""id"":4896861,""code"":""US.IL"",""name"":""Illinois""},""admin2_division"":{""id"":4890213,""code"":""US.IL.043"",""name"":""DuPage County""},""timezone"":""America/Chicago"",""names"":{""en"":""Elmhurst""},""country_names"":{""en"":""United States""},""admin1_names"":{""en"":""Illinois""},""admin2_names"":{""en"":""DuPage County""},""population"":45957}, ""countryrecord"":{""info"":{""iso"":""US"",""iso3"":""USA"",""iso_numeric"":""840"",""fips"":""US"",""name"":""United States"",""capital"":""Washington"",""area"":""9629091"",""population"":327167434,""continent"":""NA"",""tld"":"".us"",""currency_code"":""USD"",""currency_name"":""Dollar"",""phone"":""1"",""postal_code_format"":""#####-####"",""postal_code_regex"":""^\\d{5}(-\\d{4})?$"",""languages"":""en-US,es-US,haw,fr"",""geonameid"":6252001,""neighbours"":""CA,MX,CU"",""equivalent_fips_code"":""""},""names"":{""en"":""United States""},""capital_names"":{""en"":""Washington D.C.""}}, ""us_fips_codes"":{""us_state_code"":""IL"",""us_state_name"":""Illinois"",""us_state_fips_code"":""17"",""us_county"":""DuPage County"",""us_county_fips_code"":""043""}}"
"{""cityrecord"":{""id"":4154008,""name"":""East Lake"",""latitude"":28.110849380493164,""longitude"":-82.69481658935547,""country"":{""id"":6252001,""code"":""US"",""name"":""United States""},""admin_division"":{""id"":4155751,""code"":""US.FL"",""name"":""Florida""},""admin2_division"":{""id"":4168618,""code"":""US.FL.103"",""name"":""Pinellas County""},""timezone"":""America/New_York"",""names"":null,""country_names"":{""en"":""United States""},""admin1_names"":{""en"":""Florida""},""admin2_names"":{""en"":""Pinellas""},""population"":30962}, ""countryrecord"":{""info"":{""iso"":""US"",""iso3"":""USA"",""iso_numeric"":""840"",""fips"":""US"",""name"":""United States"",""capital"":""Washington"",""area"":""9629091"",""population"":327167434,""continent"":""NA"",""tld"":"".us"",""currency_code"":""USD"",""currency_name"":""Dollar"",""phone"":""1"",""postal_code_format"":""#####-####"",""postal_code_regex"":""^\\d{5}(-\\d{4})?$"",""languages"":""en-US,es-US,haw,fr"",""geonameid"":6252001,""neighbours"":""CA,MX,CU"",""equivalent_fips_code"":""""},""names"":{""en"":""United States""},""capital_names"":{""en"":""Washington D.C.""}}, ""us_fips_codes"":{""us_state_code"":""FL"",""us_state_name"":""Florida"",""us_state_fips_code"":""12"",""us_county"":""Pinellas"",""us_county_fips_code"":""003""}}"
"{""cityrecord"":{""id"":5128581,""name"":""New York City"",""latitude"":40.714271545410156,""longitude"":-74.00596618652344,""country"":{""id"":6252001,""code"":""US"",""name"":""United States""},""admin_division"":{""id"":5128638,""code"":""US.NY"",""name"":""New York""},""admin2_division"":null,""timezone"":""America/New_York"",""names"":{""en"":""New York""},""country_names"":{""en"":""United States""},""admin1_names"":{""en"":""New York""},""admin2_names"":null,""population"":8804190}, ""countryrecord"":{""info"":{""iso"":""US"",""iso3"":""USA"",""iso_numeric"":""840"",""fips"":""US"",""name"":""United States"",""capital"":""Washington"",""area"":""9629091"",""population"":327167434,""continent"":""NA"",""tld"":"".us"",""currency_code"":""USD"",""currency_name"":""Dollar"",""phone"":""1"",""postal_code_format"":""#####-####"",""postal_code_regex"":""^\\d{5}(-\\d{4})?$"",""languages"":""en-US,es-US,haw,fr"",""geonameid"":6252001,""neighbours"":""CA,MX,CU"",""equivalent_fips_code"":""""},""names"":{""en"":""United States""},""capital_names"":{""en"":""Washington D.C.""}}, ""us_fips_codes"":{""us_state_code"":""NY"",""us_state_name"":""New York"",""us_state_fips_code"":""36"",""us_county"":"""",""us_county_fips_code"":null}}"
"{""cityrecord"":{""id"":4833425,""name"":""East Haven"",""latitude"":41.27621078491211,""longitude"":-72.8684310913086,""country"":{""id"":6252001,""code"":""US"",""name"":""United States""},""admin_division"":{""id"":4831725,""code"":""US.CT"",""name"":""Connecticut""},""admin2_division"":{""id"":12809023,""code"":""US.CT.170"",""name"":""South Central Connecticut Planning Region""},""timezone"":""America/New_York"",""names"":{""en"":""East Haven""},""country_names"":{""en"":""United States""},""admin1_names"":{""en"":""Connecticut""},""admin2_names"":null,""population"":29257}, ""countryrecord"":{""info"":{""iso"":""US"",""iso3"":""USA"",""iso_numeric"":""840"",""fips"":""US"",""name"":""United States"",""capital"":""Washington"",""area"":""9629091"",""population"":327167434,""continent"":""NA"",""tld"":"".us"",""currency_code"":""USD"",""currency_name"":""Dollar"",""phone"":""1"",""postal_code_format"":""#####-####"",""postal_code_regex"":""^\\d{5}(-\\d{4})?$"",""languages"":""en-US,es-US,haw,fr"",""geonameid"":6252001,""neighbours"":""CA,MX,CU"",""equivalent_fips_code"":""""},""names"":{""en"":""United States""},""capital_names"":{""en"":""Washington D.C.""}}, ""us_fips_codes"":{""us_state_code"":""CT"",""us_state_name"":""Connecticut"",""us_state_fips_code"":""09"",""us_county"":"""",""us_county_fips_code"":""070""}}"
This is not a Location and it will not be geocoded
"40.71427, -74.00597"
"{""cityrecord"":{""id"":1703417,""name"":""Makati City"",""latitude"":14.550270080566406,""longitude"":121.0326919555664,""country"":{""id"":1694008,""code"":""PH"",""name"":""Philippines""},""admin_division"":{""id"":7521311,""code"":""PH.NCR"",""name"":""National Capital Region""},""admin2_division"":{""id"":11395838,""code"":""PH.NCR.137600000"",""name"":""Southern Manila District""},""timezone"":""Asia/Manila"",""names"":{""en"":""Makati City""},""country_names"":{""en"":""Philippines""},""admin1_names"":{""en"":""National Capital Region""},""admin2_names"":null,""population"":510383}, ""countryrecord"":{""info"":{""iso"":""PH"",""iso3"":""PHL"",""iso_numeric"":""608"",""fips"":""RP"",""name"":""Philippines"",""capital"":""Manila"",""area"":""300000"",""population"":106651922,""continent"":""AS"",""tld"":"".ph"",""currency_code"":""PHP"",""currency_name"":""Peso"",""phone"":""63"",""postal_code_format"":""####"",""postal_code_regex"":""^(\\d{4})$"",""languages"":""tl,en-PH,fil,ceb,ilo,hil,war,pam,bik,bcl,pag,mrw,tsg,mdh,cbk,krj,sgd,msb,akl,ibg,yka,mta,abx"",""geonameid"":1694008,""neighbours"":"""",""equivalent_fips_code"":""""},""names"":{""en"":""Philippines""},""capital_names"":{""en"":""Manila""}}, ""us_fips_codes"":{""us_state_code"":null,""us_state_name"":""National Capital Region"",""us_state_fips_code"":null,""us_county"":"""",""us_county_fips_code"":null}}""######;
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_fmt_cityrecord() {
    let wrk = Workdir::new("geocode_suggest_fmt_cityrecord");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Elmhurst, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["East Harlem, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["40.71427, -74.00597"],
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .arg("--formatstr")
        .arg("%cityrecord")
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec![
            "ArchivedCitiesRecord { id: 4891010, name: \"Elmhurst\", latitude: 41.89947, \
             longitude: -87.94034, country: Some(ArchivedCountry { id: 6252001, code: \"US\", \
             name: \"United States\" }), admin_division: Some(ArchivedAdminDivision { id: \
             4896861, code: \"US.IL\", name: \"Illinois\" }), admin2_division: \
             Some(ArchivedAdminDivision { id: 4890213, code: \"US.IL.043\", name: \"DuPage \
             County\" }), timezone: \"America/Chicago\", names: Some({\"en\": \"Elmhurst\"}), \
             country_names: Some({\"en\": \"United States\"}), admin1_names: Some({\"en\": \
             \"Illinois\"}), admin2_names: Some({\"en\": \"DuPage County\"}), population: 45957 }"
        ],
        svec![
            "ArchivedCitiesRecord { id: 4154008, name: \"East Lake\", latitude: 28.11085, \
             longitude: -82.69482, country: Some(ArchivedCountry { id: 6252001, code: \"US\", \
             name: \"United States\" }), admin_division: Some(ArchivedAdminDivision { id: \
             4155751, code: \"US.FL\", name: \"Florida\" }), admin2_division: \
             Some(ArchivedAdminDivision { id: 4168618, code: \"US.FL.103\", name: \"Pinellas \
             County\" }), timezone: \"America/New_York\", names: None, country_names: \
             Some({\"en\": \"United States\"}), admin1_names: Some({\"en\": \"Florida\"}), \
             admin2_names: Some({\"en\": \"Pinellas\"}), population: 30962 }"
        ],
        svec![
            "ArchivedCitiesRecord { id: 5128581, name: \"New York City\", latitude: 40.71427, \
             longitude: -74.00597, country: Some(ArchivedCountry { id: 6252001, code: \"US\", \
             name: \"United States\" }), admin_division: Some(ArchivedAdminDivision { id: \
             5128638, code: \"US.NY\", name: \"New York\" }), admin2_division: None, timezone: \
             \"America/New_York\", names: Some({\"en\": \"New York\"}), country_names: \
             Some({\"en\": \"United States\"}), admin1_names: Some({\"en\": \"New York\"}), \
             admin2_names: None, population: 8804190 }"
        ],
        svec![
            "ArchivedCitiesRecord { id: 4833425, name: \"East Haven\", latitude: 41.27621, \
             longitude: -72.86843, country: Some(ArchivedCountry { id: 6252001, code: \"US\", \
             name: \"United States\" }), admin_division: Some(ArchivedAdminDivision { id: \
             4831725, code: \"US.CT\", name: \"Connecticut\" }), admin2_division: \
             Some(ArchivedAdminDivision { id: 12809023, code: \"US.CT.170\", name: \"South \
             Central Connecticut Planning Region\" }), timezone: \"America/New_York\", names: \
             Some({\"en\": \"East Haven\"}), country_names: Some({\"en\": \"United States\"}), \
             admin1_names: Some({\"en\": \"Connecticut\"}), admin2_names: None, population: 29257 \
             }"
        ],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["40.71427, -74.00597"],
        svec![
            "ArchivedCitiesRecord { id: 1703417, name: \"Makati City\", latitude: 14.55027, \
             longitude: 121.03269, country: Some(ArchivedCountry { id: 1694008, code: \"PH\", \
             name: \"Philippines\" }), admin_division: Some(ArchivedAdminDivision { id: 7521311, \
             code: \"PH.NCR\", name: \"National Capital Region\" }), admin2_division: \
             Some(ArchivedAdminDivision { id: 11395838, code: \"PH.NCR.137600000\", name: \
             \"Southern Manila District\" }), timezone: \"Asia/Manila\", names: Some({\"en\": \
             \"Makati City\"}), country_names: Some({\"en\": \"Philippines\"}), admin1_names: \
             Some({\"en\": \"National Capital Region\"}), admin2_names: None, population: 510383 }"
        ],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_reverse() {
    let wrk = Workdir::new("geocode_reverse");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["40.812126, -73.9041813"],
            svec!["40.66472342, -73.93867227"],
            svec!["(40.766672, -73.9568128)"],
            svec!["(  40.819342, -73.9532127    )"],
            svec!["< 40.819342,-73.9532127 >"],
            svec!["This is not a Location and it will not be geocoded"],
            svec![
                "The treasure is at these coordinates 40.66472342, -73.93867227. This should be \
                 geocoded."
            ],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec![
                "The coordinates are 40.66472342 latitude, -73.93867227 longitudue. This should \
                 NOT be geocoded."
            ],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("reverse").arg("Location").arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["The Bronx, New York US"],
        svec!["Brooklyn, New York US"],
        svec!["Manhattan, New York US"],
        svec!["Cliffside Park, New Jersey US"],
        svec!["Cliffside Park, New Jersey US"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["Brooklyn, New York US"],
        svec!["95.213424, 190,1234565"],
        svec![
            "The coordinates are 40.66472342 latitude, -73.93867227 longitudue. This should NOT \
             be geocoded."
        ],
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_reverse_fmtstring() {
    let wrk = Workdir::new("geocode_reverse_fmtstring");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["40.812126, -73.9041813"],
            svec!["40.66472342, -73.93867227"],
            svec!["(40.766672, -73.9568128)"],
            svec!["(40.819342, -73.9532127)"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["95.213424, 190,1234565"], // invalid lat,long
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("reverse")
        .arg("Location")
        .arg("--formatstr")
        .arg("%city-state-country")
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["The Bronx, New York US"],
        svec!["Brooklyn, New York US"],
        svec!["Manhattan, New York US"],
        svec!["Cliffside Park, New Jersey US"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["95.213424, 190,1234565"], // invalid lat,long
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_reverse_fmtstring_intl() {
    let wrk = Workdir::new("geocode_reverse_fmtstring_intl");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["41.390205, 2.154007"],
            svec!["52.371807, 4.896029"],
            svec!["(52.520008, 13.404954)"],
            svec!["(14.55027,121.03269)"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["95.213424, 190,1234565"], // invalid lat,long
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("reverse")
        .arg("Location")
        .arg("--formatstr")
        .arg("%city-admin1-country")
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Barcelona, Catalonia ES"],
        svec!["Amsterdam, North Holland NL"],
        svec!["Berlin, State of Berlin DE"],
        svec!["Makati City, National Capital Region PH"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["95.213424, 190,1234565"], // invalid lat,long
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_reverse_fmtstring_intl_dynfmt() {
    let wrk = Workdir::new("geocode_reverse_fmtstring_intl_dynfmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["41.390205, 2.154007"],
            svec!["52.371807, 4.896029"],
            svec!["(52.520008, 13.404954)"],
            svec!["(14.55027,121.03269)"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["95.213424, 190,1234565"], // invalid lat,long
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("reverse")
        .arg("Location")
        .arg("--formatstr")
        .arg("pop: {population} tz: {timezone}")
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["pop: 1686208 tz: Europe/Madrid"],
        svec!["pop: 741636 tz: Europe/Amsterdam"],
        svec!["pop: 3426354 tz: Europe/Berlin"],
        svec!["pop: 510383 tz: Asia/Manila"],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["95.213424, 190,1234565"], // invalid lat,long
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_reverse_fmtstring_intl_invalid_dynfmt() {
    let wrk = Workdir::new("geocode_reverse_fmtstring_intl_invalid_dynfmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["41.390205, 2.154007"],
            svec!["52.371807, 4.896029"],
            svec!["(52.520008, 13.404954)"],
            svec!["(14.55027,121.03269)"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["95.213424, 190,1234565"], // invalid lat,long
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("reverse")
        .arg("Location")
        .arg("--formatstr")
        .arg("pop: {population} tz: {timezone} {doesnotexistfield}")
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Invalid dynfmt template."],
        svec!["Invalid dynfmt template."],
        svec!["Invalid dynfmt template."],
        svec!["Invalid dynfmt template."],
        svec!["This is not a Location and it will not be geocoded"],
        svec!["95.213424, 190,1234565"], // invalid lat,long
    ];
    assert_eq!(got, expected);
}

#[test]
fn geocode_suggest_dyncols_fmt() {
    let wrk = Workdir::new("geocode_suggest_dyncols_fmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["Melrose, New York"],
            svec!["East Flatbush, New York"],
            svec!["Manhattan, New York"],
            svec!["Brooklyn, New York"],
            svec!["East Harlem, New York"],
            svec!["This is not a Location and it will not be geocoded"],
            svec!["Jersey City, New Jersey"],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec!["Makati, Metro Manila, Philippines"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("suggest")
        .arg("Location")
        .args([
            "-f",
            "%dyncols: {city_col:name}, {state_col:admin1}, {county_col:admin2}, \
             {country_col:country}, {continent_col:continent}, {currency_col:currency_code}",
        ])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec![
            "Location",
            "city_col",
            "state_col",
            "county_col",
            "country_col",
            "continent_col",
            "currency_col"
        ],
        svec![
            "Melrose, New York",
            "Melrose Park",
            "Illinois",
            "Cook",
            "US",
            "NA",
            "USD"
        ],
        svec![
            "East Flatbush, New York",
            "East Lake",
            "Florida",
            "Pinellas",
            "US",
            "NA",
            "USD"
        ],
        svec![
            "Manhattan, New York",
            "New York",
            "New York",
            "",
            "US",
            "NA",
            "USD"
        ],
        svec![
            "Brooklyn, New York",
            "Brooklyn Park",
            "Minnesota",
            "Hennepin",
            "US",
            "NA",
            "USD"
        ],
        svec![
            "East Harlem, New York",
            "East Haven",
            "Connecticut",
            "",
            "US",
            "NA",
            "USD"
        ],
        svec![
            "This is not a Location and it will not be geocoded",
            "",
            "",
            "",
            "",
            "",
            ""
        ],
        svec![
            "Jersey City, New Jersey",
            "Jersey City",
            "New Jersey",
            "Hudson",
            "US",
            "NA",
            "USD"
        ],
        svec!["95.213424, 190,1234565", "", "", "", "", "", ""],
        svec![
            "Makati, Metro Manila, Philippines",
            "Makati City",
            "National Capital Region",
            "",
            "PH",
            "AS",
            "PHP"
        ],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
fn geocode_reverse_dyncols_fmt() {
    let wrk = Workdir::new("geocode_reverse_dyncols_fmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["40.812126, -73.9041813"],
            svec!["40.66472342, -73.93867227"],
            svec!["(40.766672, -73.9568128)"],
            svec!["(  40.819342, -73.9532127    )"],
            svec!["< 40.819342,-73.9532127 >"],
            svec!["This is not a Location and it will not be geocoded"],
            svec![
                "The treasure is at these coordinates 40.66472342, -73.93867227. This should be \
                 geocoded."
            ],
            svec!["95.213424, 190,1234565"], // invalid lat, long
            svec![
                "The coordinates are 40.66472342 latitude, -73.93867227 longitudue. This should \
                 NOT be geocoded."
            ],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("reverse")
        .arg("Location")
        .args([
            "-f",
            "%dyncols: {city_col:name}, {tz_col:timezone}, {capital_col:capital}, \
             {pop_col:population}",
        ])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location", "city_col", "tz_col", "capital_col", "pop_col"],
        svec![
            "40.812126, -73.9041813",
            "The Bronx",
            "America/New_York",
            "Washington",
            "1385108"
        ],
        svec![
            "40.66472342, -73.93867227",
            "Brooklyn",
            "America/New_York",
            "Washington",
            "2736074"
        ],
        svec![
            "(40.766672, -73.9568128)",
            "Manhattan",
            "America/New_York",
            "Washington",
            "1487536"
        ],
        svec![
            "(  40.819342, -73.9532127    )",
            "Cliffside Park",
            "America/New_York",
            "Washington",
            "24857"
        ],
        svec![
            "< 40.819342,-73.9532127 >",
            "Cliffside Park",
            "America/New_York",
            "Washington",
            "24857"
        ],
        svec![
            "This is not a Location and it will not be geocoded",
            "",
            "",
            "",
            ""
        ],
        svec![
            "The treasure is at these coordinates 40.66472342, -73.93867227. This should be \
             geocoded.",
            "Brooklyn",
            "America/New_York",
            "Washington",
            "2736074"
        ],
        svec!["95.213424, 190,1234565", "", "", "", ""],
        svec![
            "The coordinates are 40.66472342 latitude, -73.93867227 longitudue. This should NOT \
             be geocoded.",
            "",
            "",
            "",
            ""
        ],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
fn geocode_countryinfo() {
    let wrk = Workdir::new("geocode_countryinfo");
    wrk.create(
        "data.csv",
        vec![
            svec!["Country"],
            svec!["US"],
            svec!["CA"],
            svec!["MX"],
            svec!["us"],
            svec!["Cn"],
            svec!["This is not a country and it will not be geocoded"],
            svec!["PH"],
            svec!["95.213424, 190,1234565"],
            svec!["Germany"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("countryinfo").arg("Country").arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Country"],
        svec!["United States"],
        svec!["Canada"],
        svec!["Mexico"],
        svec!["United States"],
        svec!["China"],
        svec!["This is not a country and it will not be geocoded"],
        svec!["Philippines"],
        svec!["95.213424, 190,1234565"],
        svec!["Germany"], // passed thru as its not a valid country code
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
fn geocode_countryinfo_formatstr() {
    let wrk = Workdir::new("geocode_countryinfo_formatstr");
    wrk.create(
        "data.csv",
        vec![
            svec!["Country"],
            svec!["US"],
            svec!["CA"],
            svec!["MX"],
            svec!["us"],
            svec!["Cn"],
            svec!["This is not a country and it will not be geocoded"],
            svec!["PH"],
            svec!["95.213424, 190,1234565"],
            svec!["Germany"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("countryinfo")
        .arg("Country")
        .args([
            "--formatstr",
            "{country_name} Pop: {country_population} in {continent} using {currency_name} all in \
             {area} square kms.",
        ])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Country"],
        svec!["United States Pop: 327167434 in NA using Dollar all in 9629091 square kms."],
        svec!["Canada Pop: 37058856 in NA using Dollar all in 9984670 square kms."],
        svec!["Mexico Pop: 126190788 in NA using Peso all in 1972550 square kms."],
        svec!["United States Pop: 327167434 in NA using Dollar all in 9629091 square kms."],
        svec!["China Pop: 1411778724 in AS using Yuan Renminbi all in 9596960 square kms."],
        svec!["This is not a country and it will not be geocoded"],
        svec!["Philippines Pop: 106651922 in AS using Peso all in 300000 square kms."],
        svec!["95.213424, 190,1234565"],
        svec!["Germany"],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
fn geocode_countryinfo_formatstr_pretty_json() {
    let wrk = Workdir::new("geocode_countryinfo_formatstr_pretty_json");
    wrk.create(
        "data.csv",
        vec![
            svec!["Country"],
            svec!["US"],
            svec!["CA"],
            svec!["MX"],
            svec!["us"],
            svec!["Cn"],
            svec!["This is not a country and it will not be geocoded"],
            svec!["PH"],
            svec!["95.213424, 190,1234565"],
            svec!["Germany"],
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("countryinfo")
        .arg("Country")
        .args(["--formatstr", "%pretty-json"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);

    let expected = vec![
        svec!["Country"],
        svec![
            "{\n  \"info\": {\n    \"iso\": \"US\",\"iso3\": \"USA\",\"iso_numeric\": \
             \"840\",\"fips\": \"US\",\"name\": \"United States\",\"capital\": \
             \"Washington\",\"area\": \"9629091\",\"population\": 327167434,\"continent\": \
             \"NA\",\"tld\": \".us\",\"currency_code\": \"USD\",\"currency_name\": \
             \"Dollar\",\"phone\": \"1\",\"postal_code_format\": \
             \"#####-####\",\"postal_code_regex\": \"^\\\\d{5}(-\\\\d{4})?$\",\"languages\": \
             \"en-US,es-US,haw,fr\",\"geonameid\": 6252001,\"neighbours\": \
             \"CA,MX,CU\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \"United \
             States\"\n  },\"capital_names\": {\n    \"en\": \"Washington D.C.\"\n  }\n}"
        ],
        svec![
            "{\n  \"info\": {\n    \"iso\": \"CA\",\"iso3\": \"CAN\",\"iso_numeric\": \
             \"124\",\"fips\": \"CA\",\"name\": \"Canada\",\"capital\": \"Ottawa\",\"area\": \
             \"9984670\",\"population\": 37058856,\"continent\": \"NA\",\"tld\": \
             \".ca\",\"currency_code\": \"CAD\",\"currency_name\": \"Dollar\",\"phone\": \
             \"1\",\"postal_code_format\": \"@#@ #@#\",\"postal_code_regex\": \
             \"^([ABCEGHJKLMNPRSTVXY]\\\\d[ABCEGHJKLMNPRSTVWXYZ]) \
             ?(\\\\d[ABCEGHJKLMNPRSTVWXYZ]\\\\d)$ \",\"languages\": \
             \"en-CA,fr-CA,iu\",\"geonameid\": 6251999,\"neighbours\": \
             \"US\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \"Canada\"\n  \
             },\"capital_names\": {\n    \"en\": \"Ottawa\"\n  }\n}"
        ],
        svec![
            "{\n  \"info\": {\n    \"iso\": \"MX\",\"iso3\": \"MEX\",\"iso_numeric\": \
             \"484\",\"fips\": \"MX\",\"name\": \"Mexico\",\"capital\": \"Mexico City\",\"area\": \
             \"1972550\",\"population\": 126190788,\"continent\": \"NA\",\"tld\": \
             \".mx\",\"currency_code\": \"MXN\",\"currency_name\": \"Peso\",\"phone\": \
             \"52\",\"postal_code_format\": \"#####\",\"postal_code_regex\": \
             \"^(\\\\d{5})$\",\"languages\": \"es-MX\",\"geonameid\": 3996063,\"neighbours\": \
             \"GT,US,BZ\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \
             \"Mexico\"\n  },\"capital_names\": {\n    \"en\": \"Mexico City\"\n  }\n}"
        ],
        svec![
            "{\n  \"info\": {\n    \"iso\": \"US\",\"iso3\": \"USA\",\"iso_numeric\": \
             \"840\",\"fips\": \"US\",\"name\": \"United States\",\"capital\": \
             \"Washington\",\"area\": \"9629091\",\"population\": 327167434,\"continent\": \
             \"NA\",\"tld\": \".us\",\"currency_code\": \"USD\",\"currency_name\": \
             \"Dollar\",\"phone\": \"1\",\"postal_code_format\": \
             \"#####-####\",\"postal_code_regex\": \"^\\\\d{5}(-\\\\d{4})?$\",\"languages\": \
             \"en-US,es-US,haw,fr\",\"geonameid\": 6252001,\"neighbours\": \
             \"CA,MX,CU\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \"United \
             States\"\n  },\"capital_names\": {\n    \"en\": \"Washington D.C.\"\n  }\n}"
        ],
        svec![
            "{\n  \"info\": {\n    \"iso\": \"CN\",\"iso3\": \"CHN\",\"iso_numeric\": \
             \"156\",\"fips\": \"CH\",\"name\": \"China\",\"capital\": \"Beijing\",\"area\": \
             \"9596960\",\"population\": 1411778724,\"continent\": \"AS\",\"tld\": \
             \".cn\",\"currency_code\": \"CNY\",\"currency_name\": \"Yuan Renminbi\",\"phone\": \
             \"86\",\"postal_code_format\": \"######\",\"postal_code_regex\": \
             \"^(\\\\d{6})$\",\"languages\": \"zh-CN,yue,wuu,dta,ug,za\",\"geonameid\": \
             1814991,\"neighbours\": \
             \"LA,BT,TJ,KZ,MN,AF,NP,MM,KG,PK,KP,RU,VN,IN\",\"equivalent_fips_code\": \"\"\n  \
             },\"names\": {\n    \"en\": \"China\"\n  },\"capital_names\": {\n    \"en\": \
             \"Beijing\"\n  }\n}"
        ],
        svec!["This is not a country and it will not be geocoded"],
        svec![
            "{\n  \"info\": {\n    \"iso\": \"PH\",\"iso3\": \"PHL\",\"iso_numeric\": \
             \"608\",\"fips\": \"RP\",\"name\": \"Philippines\",\"capital\": \"Manila\",\"area\": \
             \"300000\",\"population\": 106651922,\"continent\": \"AS\",\"tld\": \
             \".ph\",\"currency_code\": \"PHP\",\"currency_name\": \"Peso\",\"phone\": \
             \"63\",\"postal_code_format\": \"####\",\"postal_code_regex\": \
             \"^(\\\\d{4})$\",\"languages\": \
             \"tl,en-PH,fil,ceb,ilo,hil,war,pam,bik,bcl,pag,mrw,tsg,mdh,cbk,krj,sgd,msb,akl,ibg,\
             yka,mta,abx\",\"geonameid\": 1694008,\"neighbours\": \"\",\"equivalent_fips_code\": \
             \"\"\n  },\"names\": {\n    \"en\": \"Philippines\"\n  },\"capital_names\": {\n    \
             \"en\": \"Manila\"\n  }\n}"
        ],
        svec!["95.213424, 190,1234565"],
        svec!["Germany"],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
fn geocode_countryinfonow() {
    let wrk = Workdir::new("geocode_countryinfonow");
    let mut cmd = wrk.command("geocode");
    cmd.arg("countryinfonow").arg("US");

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    wrk.assert_success(&mut cmd);
    let expected = vec![svec!["Location"], svec!["United States"]];
    assert_eq!(got, expected);
}

#[test]
#[serial]
fn geocode_countryinfonow_formatstr() {
    let wrk = Workdir::new("geocode_countryinfonow_formatstr");

    let mut cmd = wrk.command("geocode");
    cmd.arg("countryinfonow").arg("cA").args([
        "--formatstr",
        "{country_name} Pop: {country_population} in {continent} using {currency_name} all in \
         {area} square kms.",
    ]);

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Canada Pop: 37058856 in NA using Dollar all in 9984670 square kms."],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
fn geocode_countryinfonow_formatstr_pretty_json() {
    let wrk = Workdir::new("geocode_countryinfonow_formatstr_pretty_json");
    let mut cmd = wrk.command("geocode");
    cmd.arg("countryinfonow")
        .arg("mx")
        .args(["--formatstr", "%pretty-json"]);

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    let expected = "{\n  \"info\": {\n    \"iso\": \"MX\",\"iso3\": \"MEX\",\"iso_numeric\": \
                    \"484\",\"fips\": \"MX\",\"name\": \"Mexico\",\"capital\": \"Mexico \
                    City\",\"area\": \"1972550\",\"population\": 126190788,\"continent\": \
                    \"NA\",\"tld\": \".mx\",\"currency_code\": \"MXN\",\"currency_name\": \
                    \"Peso\",\"phone\": \"52\",\"postal_code_format\": \
                    \"#####\",\"postal_code_regex\": \"^(\\\\d{5})$\",\"languages\": \
                    \"es-MX\",\"geonameid\": 3996063,\"neighbours\": \
                    \"GT,US,BZ\",\"equivalent_fips_code\": \"\"\n  },\"names\": {\n    \"en\": \
                    \"Mexico\"\n  },\"capital_names\": {\n    \"en\": \"Mexico City\"\n  }\n}";
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookup() {
    let wrk = Workdir::new("geocode_iplookup");
    wrk.create(
        "data.csv",
        vec![
            svec!["IP"],
            svec!["8.8.8.8"],        // Google DNS
            svec!["1.1.1.1"],        // Cloudflare DNS
            svec!["208.67.222.222"], // OpenDNS
            svec!["This is not an IP and it will not be geocoded"],
            svec!["192.168.1.1"],        // Private IP
            svec!["127.0.0.1"],          // Localhost
            svec!["invalid-ip-address"], // Invalid IP
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookup").arg("IP").arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["IP"],
        svec!["8.8.8.8"],
        svec!["1.1.1.1"],
        svec!["San Francisco, California US"],
        svec!["This is not an IP and it will not be geocoded"],
        svec!["192.168.1.1"],        // Private IP returns as-is
        svec!["127.0.0.1"],          // Localhost returns as-is
        svec!["invalid-ip-address"], // Invalid IP returns as-is
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookup_formatstr() {
    let wrk = Workdir::new("geocode_iplookup_formatstr");
    wrk.create(
        "data.csv",
        vec![
            svec!["IP"],
            svec!["3.3.3.3"], // Amazon
            svec!["This is not an IP and it will not be geocoded"],
            svec!["192.168.1.1"], // Private IP
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookup")
        .arg("IP")
        .args(["--formatstr", "%city-state-country"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["IP"],
        svec!["Ashburn, Virginia US"],
        svec!["This is not an IP and it will not be geocoded"],
        svec!["192.168.1.1"], // Private IP returns as-is
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookup_formatstr_dynfmt() {
    let wrk = Workdir::new("geocode_iplookup_formatstr_dynfmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["IP"],
            svec!["3.3.3.3"], // Amazon
            svec!["This is not an IP and it will not be geocoded"],
            svec!["192.168.1.1"], // Private IP
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookup")
        .arg("IP")
        .args([
            "--formatstr",
            "City: {name}, State: {admin1}, Country: {country} - {timezone}",
        ])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["IP"],
        svec!["City: Ashburn, State: Virginia, Country: US - America/New_York"],
        svec!["This is not an IP and it will not be geocoded"],
        svec!["192.168.1.1"], // Private IP returns as-is
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookup_formatstr_json() {
    let wrk = Workdir::new("geocode_iplookup_formatstr_json");
    wrk.create(
        "data.csv",
        vec![
            svec!["IP"],
            svec!["3.3.3.3"], // Amazon
            svec!["This is not an IP and it will not be geocoded"],
            svec!["192.168.1.1"], // Private IP
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookup")
        .arg("IP")
        .args(["--formatstr", "%json"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    // The JSON output will contain the full city record, so we just check that it contains expected
    // fields
    assert!(got[1][0].contains("\"cityrecord\""));
    assert!(got[1][0].contains("\"countryrecord\""));
    assert!(got[1][0].contains("\"us_fips_codes\""));
    assert_eq!(got[2][0], "This is not an IP and it will not be geocoded");
    assert_eq!(got[3][0], "192.168.1.1");
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookup_formatstr_pretty_json() {
    let wrk = Workdir::new("geocode_iplookup_formatstr_pretty_json");
    wrk.create(
        "data.csv",
        vec![
            svec!["IP"],
            svec!["3.3.3.3"], // Amazon
            svec!["This is not an IP and it will not be geocoded"],
            svec!["192.168.1.1"], // Private IP
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookup")
        .arg("IP")
        .args(["--formatstr", "%pretty-json"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    // The pretty JSON output will contain the full city record with proper formatting
    assert!(got[1][0].contains("\n  \"cityrecord\""));
    assert!(got[1][0].contains("\n  \"countryrecord\""));
    assert_eq!(got[2][0], "This is not an IP and it will not be geocoded");
    assert_eq!(got[3][0], "192.168.1.1");
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookup_formatstr_cityrecord() {
    let wrk = Workdir::new("geocode_iplookup_formatstr_cityrecord");
    wrk.create(
        "data.csv",
        vec![
            svec!["IP"],
            svec!["3.3.3.3"], // Amazon
            svec!["This is not an IP and it will not be geocoded"],
            svec!["192.168.1.1"], // Private IP
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookup")
        .arg("IP")
        .args(["--formatstr", "%cityrecord"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    // The cityrecord output will contain the full ArchivedCitiesRecord debug format
    assert!(got[1][0].contains("ArchivedCitiesRecord"));
    assert!(got[1][0].contains("name: \"Ashburn\""));
    assert!(got[1][0].contains("latitude:"));
    assert!(got[1][0].contains("longitude:"));
    assert_eq!(got[2][0], "This is not an IP and it will not be geocoded");
    assert_eq!(got[3][0], "192.168.1.1");
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookup_dyncols_fmt() {
    let wrk = Workdir::new("geocode_iplookup_dyncols_fmt");
    wrk.create(
        "data.csv",
        vec![
            svec!["IP"],
            svec!["3.3.3.3"], // Amazon
            svec!["This is not an IP and it will not be geocoded"],
            svec!["192.168.1.1"], // Private IP
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookup")
        .arg("IP")
        .args([
            "-f",
            "%dyncols: {city_col:name}, {state_col:admin1}, {country_col:country}, \
             {tz_col:timezone}",
        ])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["IP", "city_col", "state_col", "country_col", "tz_col"],
        svec!["3.3.3.3", "Ashburn", "Virginia", "US", "America/New_York"],
        svec![
            "This is not an IP and it will not be geocoded",
            "",
            "",
            "",
            ""
        ],
        svec!["192.168.1.1", "", "", "", ""],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookup_invalid_result() {
    let wrk = Workdir::new("geocode_iplookup_invalid_result");
    wrk.create(
        "data.csv",
        vec![
            svec!["IP"],
            svec!["3.3.3.3"], // Amazon
            svec!["This is not an IP and it will not be geocoded"],
            svec!["192.168.1.1"],        // Private IP
            svec!["invalid-ip-address"], // Invalid IP
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookup")
        .arg("IP")
        .args(["--invalid-result", "<NO_LOCATION>"])
        .arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["IP"],
        svec!["Ashburn, Virginia US"],
        svec!["<NO_LOCATION>"],
        svec!["192.168.1.1"],
        svec!["<NO_LOCATION>"],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow() {
    let wrk = Workdir::new("geocode_iplookupnow");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow").arg("3.3.3.3");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["Ashburn, Virginia US: 39.04372, -77.48749"],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_url() {
    let wrk = Workdir::new("geocode_iplookupnow_url");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow").arg("https://nytimes.com");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["San Francisco, California US: 37.77493, -122.41942"],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_formatstr() {
    let wrk = Workdir::new("geocode_iplookupnow_formatstr");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow")
        .arg("3.3.3.3")
        .args(["--formatstr", "%city-state-country"]);

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![svec!["Location"], svec!["Ashburn, Virginia US"]];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_formatstr_dynfmt() {
    let wrk = Workdir::new("geocode_iplookupnow_formatstr_dynfmt");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow").arg("3.3.3.3").args([
        "--formatstr",
        "City: {name}, State: {admin1}, Country: {country} - {timezone}",
    ]);

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    let expected = vec![
        svec!["Location"],
        svec!["City: Ashburn, State: Virginia, Country: US - America/New_York"],
    ];
    assert_eq!(got, expected);
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_formatstr_json() {
    let wrk = Workdir::new("geocode_iplookupnow_formatstr_json");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow")
        .arg("3.3.3.3")
        .args(["--formatstr", "%json"]);

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // The JSON output will contain the full city record
    assert!(got.contains("\"cityrecord\""));
    assert!(got.contains("\"countryrecord\""));
    assert!(got.contains("\"us_fips_codes\""));
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_formatstr_pretty_json() {
    let wrk = Workdir::new("geocode_iplookupnow_formatstr_pretty_json");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow")
        .arg("3.3.3.3")
        .args(["--formatstr", "%pretty-json"]);

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // The pretty JSON output will contain the full city record with proper formatting
    assert!(got.contains("\n  \"cityrecord\""));
    assert!(got.contains("\n  \"countryrecord\""));
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_formatstr_cityrecord() {
    let wrk = Workdir::new("geocode_iplookupnow_formatstr_cityrecord");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow")
        .arg("3.3.3.3")
        .args(["--formatstr", "%cityrecord"]);

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // The cityrecord output will contain the full ArchivedCitiesRecord debug format
    assert!(got.contains("ArchivedCitiesRecord"));
    assert!(got.contains("Ashburn"));
    assert!(got.contains("latitude:"));
    assert!(got.contains("longitude:"));
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_private_ip() {
    let wrk = Workdir::new("geocode_iplookupnow_private_ip");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow").arg("192.168.1.1");

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // Private IP should return the IP address as-is
    assert!(got.contains("192.168.1.1"));
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_localhost() {
    let wrk = Workdir::new("geocode_iplookupnow_localhost");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow").arg("127.0.0.1");

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // Localhost should return the IP address as-is
    assert!(got.contains("127.0.0.1"));
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_invalid_ip() {
    let wrk = Workdir::new("geocode_iplookupnow_invalid_ip");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow").arg("invalid-ip-address");

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // Invalid IP should return the input as-is
    assert!(got.contains("invalid-ip-address"));
}

#[test]
#[serial]
#[ignore = "GeoIP2 database requires a license and is not included in the repo"]
fn geocode_iplookupnow_invalid_url() {
    let wrk = Workdir::new("geocode_iplookupnow_invalid_url");
    let mut cmd = wrk.command("geocode");
    cmd.arg("iplookupnow").arg("not-a-valid-url");

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // Invalid URL should return the input as-is
    assert!(got.contains("not-a-valid-url"));
}

// ---------------------------------------------------------------------------
// OpenCage online geocoding tests.
// The geocode_opencage* tests below hit the live OpenCage API, so they are
// #[ignore]'d by default. Run them with a free OpenCage API key:
//   QSV_OPENCAGE_API_KEY=<key> cargo test -F all_features geocode_opencage -- --ignored
// ---------------------------------------------------------------------------

#[test]
#[serial]
fn geocode_opencage_missing_api_key() {
    // this runs in CI: no --api-key and no QSV_OPENCAGE_API_KEY -> clean error
    let wrk = Workdir::new("geocode_opencage_missing_api_key");
    let mut cmd = wrk.command("geocode");
    cmd.env_remove("QSV_OPENCAGE_API_KEY");
    cmd.arg("opencagenow").arg("Brooklyn, NY");

    // capture a single run, then assert on both status and stderr
    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("requires an API key"));
}

#[test]
#[serial]
fn geocode_opencage_admin1_rejected() {
    // this runs in CI: --admin1 is not supported by the opencage subcommands
    let wrk = Workdir::new("geocode_opencage_admin1_rejected");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["--api-key", "dummy-key-for-arg-validation"])
        .args(["--admin1", "US.NY"]);

    // capture a single run, then assert on both status and stderr
    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("--admin1"));
}

#[test]
#[serial]
#[ignore = "requires an OpenCage API key (set QSV_OPENCAGE_API_KEY)"]
fn geocode_opencage() {
    let wrk = Workdir::new("geocode_opencage");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["1600 Pennsylvania Ave NW, Washington DC"], // forward
            svec!["40.71427, -74.00597"],                     // reverse (auto-detected)
            svec![""],                                        // empty -> passthrough
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencage").arg("Location").arg("data.csv");

    wrk.assert_success(&mut cmd);

    let got: Vec<Vec<String>> = wrk.read_stdout(&mut cmd);
    assert_eq!(got.len(), 4);
    assert_eq!(got[0], svec!["Location"]);
    // forward & reverse rows are geocoded (changed from the input)
    assert_ne!(got[1][0], "1600 Pennsylvania Ave NW, Washington DC");
    assert!(!got[1][0].is_empty());
    assert_ne!(got[2][0], "40.71427, -74.00597");
    assert!(!got[2][0].is_empty());
    // empty cell is passed through untouched
    assert_eq!(got[3], svec![""]);
}

#[test]
#[serial]
#[ignore = "requires an OpenCage API key (set QSV_OPENCAGE_API_KEY)"]
fn geocode_opencagenow_forward() {
    let wrk = Workdir::new("geocode_opencagenow_forward");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["-f", "%location"]);

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // %location format is "(lat, long)"
    assert!(got.contains('(') && got.contains(','));
}

#[test]
#[serial]
#[ignore = "requires an OpenCage API key (set QSV_OPENCAGE_API_KEY)"]
fn geocode_opencagenow_reverse() {
    let wrk = Workdir::new("geocode_opencagenow_reverse");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow").arg("40.71427, -74.00597");

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // a reverse geocode of a NYC coordinate should mention New York
    assert!(got.to_lowercase().contains("new york") || got.to_lowercase().contains("york"));
}

#[test]
#[serial]
#[ignore = "requires an OpenCage API key (set QSV_OPENCAGE_API_KEY)"]
fn geocode_opencagenow_json() {
    let wrk = Workdir::new("geocode_opencagenow_json");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["-f", "%json"]);

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    // %json on a now subcommand emits valid JSON (no "Location" header)
    let parsed: serde_json::Value = serde_json::from_str(got.trim()).unwrap();
    assert!(parsed.get("formatted").is_some());
    assert!(parsed.get("geometry").is_some());
}

#[test]
#[serial]
#[ignore = "requires an OpenCage API key (set QSV_OPENCAGE_API_KEY)"]
fn geocode_opencagenow_dynfmt() {
    let wrk = Workdir::new("geocode_opencagenow_dynfmt");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["-f", "{components.country}"]);

    wrk.assert_success(&mut cmd);

    let got: String = wrk.stdout(&mut cmd);
    assert!(got.to_lowercase().contains("united states"));
}

#[test]
#[serial]
fn geocode_opencage_dyncols_invalid_key_rejected() {
    // this runs in CI: an invalid %dyncols: key is rejected before any API call
    let wrk = Workdir::new("geocode_opencage_dyncols_invalid_key_rejected");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["--api-key", "dummy-key-for-arg-validation"])
        .args(["-f", "%dyncols: {city:components.city}, {bad:not_a_field}"]);

    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("Invalid '%dyncols:' key"));
}

#[test]
#[serial]
fn geocode_opencage_dyncols_bare_prefix_rejected() {
    // this runs in CI: a bare "components."/"annotations." prefix with no
    // field suffix is rejected before any API call - it has no field to
    // resolve and would otherwise silently yield an empty column. The
    // components. and annotations. branches are checked independently
    // because validation short-circuits on the first invalid key.
    for bad_key in ["components.", "annotations."] {
        let wrk = Workdir::new("geocode_opencage_dyncols_bare_prefix_rejected");
        let mut cmd = wrk.command("geocode");
        cmd.arg("opencagenow")
            .arg("Brooklyn, NY")
            .args(["--api-key", "dummy-key-for-arg-validation"])
            .args(["-f", &format!("%dyncols: {{bad:{bad_key}}}")]);

        let output = wrk.output(&mut cmd);
        assert!(
            !output.status.success(),
            "bare prefix '{bad_key}' should be rejected"
        );
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(
            stderr.contains("Invalid '%dyncols:' key"),
            "bare prefix '{bad_key}' should fail with \"Invalid '%dyncols:' key\", got: {stderr}"
        );
    }
}

#[test]
#[serial]
fn geocode_opencage_dyncols_new_column_rejected() {
    // this runs in CI: %dyncols: cannot be combined with --new-column
    let wrk = Workdir::new("geocode_opencage_dyncols_new_column_rejected");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["--api-key", "dummy-key-for-arg-validation"])
        .args(["-c", "geocoded"])
        .args(["-f", "%dyncols: {city:components.city}"]);

    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("--new-column"));
}

#[test]
#[serial]
fn geocode_opencage_dyncols_empty_rejected() {
    // this runs in CI: a "%dyncols:" formatstr with no col_name:key pairs is rejected
    let wrk = Workdir::new("geocode_opencage_dyncols_empty_rejected");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["--api-key", "dummy-key-for-arg-validation"])
        .args(["-f", "%dyncols:"]);

    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("expected one or more"));
}

#[test]
#[serial]
fn geocode_opencage_dyncols_malformed_pair_rejected() {
    // this runs in CI: a malformed pair (no colon separator) is rejected, not
    // silently dropped
    let wrk = Workdir::new("geocode_opencage_dyncols_malformed_pair_rejected");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["--api-key", "dummy-key-for-arg-validation"])
        .args(["-f", "%dyncols: {city components.city}"]);

    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("Invalid '%dyncols:' pair"));
}

#[test]
#[serial]
fn geocode_opencage_dyncols_empty_col_name_rejected() {
    // this runs in CI: a pair with an empty column name is rejected
    let wrk = Workdir::new("geocode_opencage_dyncols_empty_col_name_rejected");
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencagenow")
        .arg("Brooklyn, NY")
        .args(["--api-key", "dummy-key-for-arg-validation"])
        .args(["-f", "%dyncols: {:components.city}"]);

    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("column name is empty"));
}

#[test]
#[serial]
#[ignore = "requires an OpenCage API key (set QSV_OPENCAGE_API_KEY)"]
fn geocode_opencage_dyncols() {
    let wrk = Workdir::new("geocode_opencage_dyncols");
    wrk.create(
        "data.csv",
        vec![
            svec!["Location"],
            svec!["1600 Pennsylvania Ave NW, Washington DC"], // forward
            svec![""],                                        // empty -> empty dyncols
        ],
    );
    let mut cmd = wrk.command("geocode");
    cmd.arg("opencage").arg("Location").arg("data.csv").args([
        "-f",
        "%dyncols: {city:components.city}, {country:components.country}, {addr:formatted}",
    ]);

    let got: Vec<Vec<String>> = wrk.read_stdout_on_success(&mut cmd);
    assert_eq!(got.len(), 3);
    // the input column is preserved & three dyncols columns are appended
    assert_eq!(got[0], svec!["Location", "city", "country", "addr"]);
    // geocoded row: input cell unchanged, dyncols populated
    assert_eq!(got[1][0], "1600 Pennsylvania Ave NW, Washington DC");
    assert!(!got[1][3].is_empty());
    assert!(got[1][2].to_lowercase().contains("united states"));
    // empty cell: input preserved, dyncols left empty
    assert_eq!(got[2], svec!["", "", "", ""]);
}

// ---------------------------------------------------------------------------
// cache-* subcommands manage the persistent on-disk OpenCage result cache.
// The tests below isolate the cache via QSV_CACHE_DIR pointed at the Workdir,
// so they never touch the real ~/.qsv-cache. The "no cache" tests run in CI;
// the populated-cache round-trip is #[ignore]'d as it needs a live API key.
// ---------------------------------------------------------------------------

#[test]
#[serial]
fn geocode_cache_info_no_cache() {
    // cache-info on a cache dir with no OpenCage cache -> graceful "no cache" report
    let wrk = Workdir::new("geocode_cache_info_no_cache");
    let mut cmd = wrk.command("geocode");
    cmd.env("QSV_CACHE_DIR", wrk.path("").to_string_lossy().to_string());
    cmd.arg("cache-info");

    let output = wrk.output(&mut cmd);
    assert!(output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("No OpenCage cache found"));
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("\"exists\":false"));
}

#[test]
#[serial]
fn geocode_cache_clear_no_cache() {
    // cache-clear on an empty cache dir -> succeeds, nothing to do
    let wrk = Workdir::new("geocode_cache_clear_no_cache");
    let mut cmd = wrk.command("geocode");
    cmd.env("QSV_CACHE_DIR", wrk.path("").to_string_lossy().to_string());
    cmd.arg("cache-clear");
    wrk.assert_success(&mut cmd);
}

#[test]
#[serial]
fn geocode_cache_prune_missing_flag() {
    // cache-prune requires --older-than -> docopt usage error
    let wrk = Workdir::new("geocode_cache_prune_missing_flag");
    let mut cmd = wrk.command("geocode");
    cmd.arg("cache-prune");
    wrk.assert_err(&mut cmd);
}

#[test]
#[serial]
fn geocode_cache_prune_bad_older_than() {
    // an unparsable --older-than value -> clean error (validated in run(),
    // independent of whether a cache exists)
    let wrk = Workdir::new("geocode_cache_prune_bad_older_than");
    let mut cmd = wrk.command("geocode");
    cmd.env("QSV_CACHE_DIR", wrk.path("").to_string_lossy().to_string());
    cmd.arg("cache-prune").args(["--older-than", "not-a-date"]);

    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("Invalid --older-than"));
}

#[test]
#[serial]
fn geocode_cache_older_than_rejected_elsewhere() {
    // --older-than is only valid with cache-prune -> rejected on cache-info
    let wrk = Workdir::new("geocode_cache_older_than_rejected_elsewhere");
    let mut cmd = wrk.command("geocode");
    cmd.arg("cache-info").args(["--older-than", "30d"]);

    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("--older-than is only valid"));
}

#[test]
#[serial]
#[ignore = "requires an OpenCage API key (set QSV_OPENCAGE_API_KEY)"]
fn geocode_cache_roundtrip() {
    // populate the cache via opencagenow, inspect it, then clear it
    let wrk = Workdir::new("geocode_cache_roundtrip");
    let cache_dir = wrk.path("").to_string_lossy().to_string();

    let mut populate = wrk.command("geocode");
    populate
        .env("QSV_CACHE_DIR", &cache_dir)
        .arg("opencagenow")
        .arg("Brooklyn, NY");
    wrk.assert_success(&mut populate);

    let mut info = wrk.command("geocode");
    info.env("QSV_CACHE_DIR", &cache_dir).arg("cache-info");
    let info_out = wrk.output(&mut info);
    assert!(info_out.status.success());
    let info_json = String::from_utf8_lossy(&info_out.stdout);
    assert!(info_json.contains("\"exists\":true"));
    assert!(!info_json.contains("\"entry_count\":0"));

    let mut clear = wrk.command("geocode");
    clear.env("QSV_CACHE_DIR", &cache_dir).arg("cache-clear");
    wrk.assert_success(&mut clear);

    let mut info2 = wrk.command("geocode");
    info2.env("QSV_CACHE_DIR", &cache_dir).arg("cache-info");
    let info2_out = wrk.output(&mut info2);
    assert!(info2_out.status.success());
    let info2_json = String::from_utf8_lossy(&info2_out.stdout);
    assert!(info2_json.contains("\"entry_count\":0"));
}

#[test]
#[serial]
fn geocode_index_load_invalid_shortcut() {
    // the index-load numeric shortcut only accepts 15000 (the prebuilt cities15000
    // index); any other number fails fast with an actionable error - no download
    let wrk = Workdir::new("geocode_index_load_invalid_shortcut");
    let mut cmd = wrk.command("geocode");
    cmd.env("QSV_CACHE_DIR", wrk.path("").to_string_lossy().to_string());
    cmd.arg("index-load").arg("5000");

    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Invalid index shortcut") && stderr.contains("only 15000 is supported"),
        "expected an actionable 'only 15000' error, got: {stderr}"
    );
}

#[test]
#[serial]
fn geocode_index_load_numeric_named_file_not_shortcut() {
    // a real local file whose name happens to be numeric (e.g. `15000.rkyv`) must be
    // loaded as a file, NOT mistaken for the `15000` download shortcut - which would
    // overwrite it. Verify the file content is left untouched.
    let wrk = Workdir::new("geocode_index_load_numeric_named_file_not_shortcut");
    let index_path = wrk.path("15000.rkyv");
    let original = b"this is not a real rkyv index";
    std::fs::write(&index_path, original).unwrap();

    let mut cmd = wrk.command("geocode");
    cmd.env("QSV_CACHE_DIR", wrk.path("").to_string_lossy().to_string());
    cmd.arg("index-load").arg(&index_path);

    // loading garbage content fails, but crucially the file must not be overwritten
    let output = wrk.output(&mut cmd);
    assert!(!output.status.success());
    assert_eq!(
        std::fs::read(&index_path).unwrap(),
        original,
        "index-load must not overwrite a real file whose name parses as a number"
    );
}
