import concurrent.futures
import os
import pickle
from array import array
from functools import partial
from glob import glob
from itertools import permutations
from pathlib import Path
from unittest.mock import call, patch

import numpy
import pytest
from numpy.testing import assert_almost_equal, assert_array_equal

import pyproj
from pyproj import CRS, Proj, Transformer, itransform, transform
from pyproj.datadir import append_data_dir
from pyproj.enums import CRSExtentUse, IntermediateCRSUse, TransformDirection
from pyproj.exceptions import ProjError
from pyproj.transformer import AreaOfInterest, TransformerGroup
from test.conftest import (
    PROJ_GTE_990,
    grids_available,
    proj_env,
    proj_network_env,
)


def test_tranform_wgs84_to_custom():
    custom_proj = pyproj.Proj(
        "+proj=geos +lon_0=0.000000 +lat_0=0 +h=35807.414063"
        " +a=6378.169000 +b=6356.583984"
    )
    wgs84 = pyproj.Proj("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
    lat, lon = 51.04715, 3.23406
    with pytest.warns(FutureWarning):
        xx, yy = pyproj.transform(wgs84, custom_proj, lon, lat)
    assert f"{xx:.3f} {yy:.3f}" == "212.623 4604.975"


@pytest.mark.grid
def test_transform_wgs84_to_alaska():
    with pytest.warns(FutureWarning):
        lat_lon_proj = pyproj.Proj(init="epsg:4326", preserve_units=False)
        alaska_aea_proj = pyproj.Proj(init="epsg:2964", preserve_units=False)
    test = (-179.72638, 49.752533)
    with pytest.warns(FutureWarning):
        xx, yy = pyproj.transform(lat_lon_proj, alaska_aea_proj, *test)
    if grids_available("us_noaa_alaska.tif"):
        assert f"{xx:.3f} {yy:.3f}" == "-1824924.495 330822.800"
    else:
        assert f"{xx:.3f} {yy:.3f}" == "-1825155.697 330730.391"


@pytest.mark.skip(reason="https://github.com/OSGeo/PROJ/issues/2425")
def test_illegal_transformation():
    # issue 202
    with pytest.warns(FutureWarning):
        p1 = pyproj.Proj(init="epsg:4326")
        p2 = pyproj.Proj(init="epsg:3857")
    with pytest.warns(FutureWarning):
        xx, yy = pyproj.transform(
            p1, p2, (-180, -180, 180, 180, -180), (-90, 90, 90, -90, -90)
        )
    assert numpy.all(numpy.isinf(xx))
    assert numpy.all(numpy.isinf(yy))
    with pytest.warns(FutureWarning), pytest.raises(ProjError):
        pyproj.transform(
            p1, p2, (-180, -180, 180, 180, -180), (-90, 90, 90, -90, -90), errcheck=True
        )


def test_lambert_conformal_transform():
    # issue 207
    with pytest.warns(FutureWarning):
        Midelt = pyproj.Proj(init="epsg:26191")
        WGS84 = pyproj.Proj(init="epsg:4326")

    E = 567623.931
    N = 256422.787
    h = 1341.467
    with pytest.warns(FutureWarning):
        Long1, Lat1, H1 = pyproj.transform(Midelt, WGS84, E, N, h, radians=False)
    assert_almost_equal((Long1, Lat1, H1), (-4.6753456, 32.902199, 1341.467), decimal=5)


def test_4d_transform(scalar_and_array):
    transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000")
    assert_almost_equal(
        transformer.transform(
            xx=scalar_and_array(3513638.19380),
            yy=scalar_and_array(778956.45250),
            zz=scalar_and_array(5248216.46900),
            tt=scalar_and_array(2008.75),
        ),
        (
            scalar_and_array(3513638.1999428216),
            scalar_and_array(778956.4532640711),
            scalar_and_array(5248216.453456361),
            scalar_and_array(2008.75),
        ),
    )


def test_2d_with_time_transform(scalar_and_array):
    transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000")
    assert_almost_equal(
        transformer.transform(
            xx=scalar_and_array(3513638.19380),
            yy=scalar_and_array(778956.45250),
            tt=scalar_and_array(2008.75),
        ),
        (
            scalar_and_array(3513638.1999428216),
            scalar_and_array(778956.4532640711),
            scalar_and_array(2008.75),
        ),
    )


def test_4d_transform_crs_obs1(scalar_and_array):
    transformer = Transformer.from_proj(7789, 8401)
    assert_almost_equal(
        transformer.transform(
            xx=scalar_and_array(3496737.2679),
            yy=scalar_and_array(743254.4507),
            zz=scalar_and_array(5264462.9620),
            tt=scalar_and_array(2019.0),
        ),
        (
            scalar_and_array(3496737.757717311),
            scalar_and_array(743253.9940103051),
            scalar_and_array(5264462.701132784),
            scalar_and_array(2019.0),
        ),
    )


def test_4d_transform_orginal_crs_obs1():
    with pytest.warns(FutureWarning):
        assert_almost_equal(
            transform(
                7789, 8401, x=3496737.2679, y=743254.4507, z=5264462.9620, tt=2019.0
            ),
            (3496737.757717311, 743253.9940103051, 5264462.701132784, 2019.0),
        )


def test_4d_transform_crs_obs2(scalar_and_array):
    transformer = Transformer.from_proj(4896, 7930)
    assert_almost_equal(
        transformer.transform(
            xx=scalar_and_array(3496737.2679),
            yy=scalar_and_array(743254.4507),
            zz=scalar_and_array(5264462.9620),
            tt=scalar_and_array(2019.0),
        ),
        (
            scalar_and_array(3496737.7857162016),
            scalar_and_array(743254.0394113371),
            scalar_and_array(5264462.643659916),
            scalar_and_array(2019.0),
        ),
    )


def test_2d_with_time_transform_crs_obs2(scalar_and_array):
    transformer = Transformer.from_proj(4896, 7930)
    assert_almost_equal(
        transformer.transform(
            xx=scalar_and_array(3496737.2679),
            yy=scalar_and_array(743254.4507),
            tt=scalar_and_array(2019.0),
        ),
        (
            scalar_and_array(3496737.4105305015),
            scalar_and_array(743254.1014318303),
            scalar_and_array(2019.0),
        ),
    )


def test_2d_with_time_transform_original_crs_obs2():
    with pytest.warns(FutureWarning):
        assert_almost_equal(
            transform(4896, 7930, x=3496737.2679, y=743254.4507, tt=2019.0),
            (3496737.4105305015, 743254.1014318303, 2019.0),
        )


def test_4d_itransform():
    transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000")
    assert_almost_equal(
        list(
            transformer.itransform(
                [(3513638.19380, 778956.45250, 5248216.46900, 2008.75)]
            )
        ),
        [(3513638.1999428216, 778956.4532640711, 5248216.453456361, 2008.75)],
    )


def test_3d_time_itransform():
    transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000")
    assert_almost_equal(
        list(
            transformer.itransform(
                [(3513638.19380, 778956.45250, 2008.75)], time_3rd=True
            )
        ),
        [(3513638.1999428216, 778956.4532640711, 2008.75)],
    )


def test_4d_itransform_orginal_crs_obs1():
    with pytest.warns(FutureWarning):
        assert_almost_equal(
            list(
                itransform(
                    7789, 8401, [(3496737.2679, 743254.4507, 5264462.9620, 2019.0)]
                )
            ),
            [(3496737.757717311, 743253.9940103051, 5264462.701132784, 2019.0)],
        )


def test_2d_with_time_itransform_original_crs_obs2():
    with pytest.warns(FutureWarning):
        assert_almost_equal(
            list(
                itransform(
                    4896, 7930, [(3496737.2679, 743254.4507, 2019.0)], time_3rd=True
                )
            ),
            [(3496737.4105305015, 743254.1014318303, 2019.0)],
        )


def test_itransform_time_3rd_invalid():
    with (
        pytest.warns(FutureWarning),
        pytest.raises(ValueError, match=r"'time_3rd' is only valid for 3 coordinates."),
    ):
        list(
            itransform(
                7789,
                8401,
                [(3496737.2679, 743254.4507, 5264462.9620, 2019.0)],
                time_3rd=True,
            )
        )
    with (
        pytest.warns(FutureWarning),
        pytest.raises(ValueError, match=r"'time_3rd' is only valid for 3 coordinates."),
    ):
        list(itransform(7789, 8401, [(3496737.2679, 743254.4507)], time_3rd=True))


def test_transform_no_error():
    with pytest.warns(FutureWarning):
        pj = Proj(init="epsg:4555")
    pjx, pjy = pj(116.366, 39.867)
    with pytest.warns(FutureWarning):
        transform(pj, Proj(4326), pjx, pjy, radians=True, errcheck=True)


def test_itransform_no_error():
    with pytest.warns(FutureWarning):
        pj = Proj(init="epsg:4555")
    pjx, pjy = pj(116.366, 39.867)
    with pytest.warns(FutureWarning):
        list(itransform(pj, Proj(4326), [(pjx, pjy)], radians=True, errcheck=True))


def test_transform_no_exception():
    # issue 249
    with pytest.warns(FutureWarning):
        transformer = Transformer.from_proj("+init=epsg:4326", "+init=epsg:27700")
    transformer.transform(1.716073972, 52.658007833, errcheck=True)
    transformer.itransform([(1.716073972, 52.658007833)], errcheck=True)


def test_transform__out_of_bounds(scalar_and_array):
    with pytest.warns(FutureWarning):
        transformer = Transformer.from_proj("+init=epsg:4326", "+init=epsg:27700")
    with pytest.raises(pyproj.exceptions.ProjError):
        transformer.transform(
            scalar_and_array(100000), scalar_and_array(100000), errcheck=True
        )


def test_transform_radians():
    with pytest.warns(FutureWarning):
        WGS84 = pyproj.Proj("+init=EPSG:4326")
    ECEF = pyproj.Proj(proj="geocent", ellps="WGS84", datum="WGS84")
    with pytest.warns(FutureWarning):
        assert_almost_equal(
            pyproj.transform(
                ECEF, WGS84, -2704026.010, -4253051.810, 3895878.820, radians=True
            ),
            (-2.137113493845668, 0.6613203738996222, -20.531156923621893),
        )

        assert_almost_equal(
            pyproj.transform(
                WGS84,
                ECEF,
                -2.137113493845668,
                0.6613203738996222,
                -20.531156923621893,
                radians=True,
            ),
            (-2704026.010, -4253051.810, 3895878.820),
        )


def test_itransform_radians():
    with pytest.warns(FutureWarning):
        WGS84 = pyproj.Proj("+init=EPSG:4326")
    ECEF = pyproj.Proj(proj="geocent", ellps="WGS84", datum="WGS84")
    with pytest.warns(FutureWarning):
        assert_almost_equal(
            list(
                pyproj.itransform(
                    ECEF,
                    WGS84,
                    [(-2704026.010, -4253051.810, 3895878.820)],
                    radians=True,
                )
            ),
            [(-2.137113493845668, 0.6613203738996222, -20.531156923621893)],
        )

        assert_almost_equal(
            list(
                pyproj.itransform(
                    WGS84,
                    ECEF,
                    [(-2.137113493845668, 0.6613203738996222, -20.531156923621893)],
                    radians=True,
                )
            ),
            [(-2704026.010, -4253051.810, 3895878.820)],
        )


def test_4d_transform__inverse(scalar_and_array):
    transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000")
    assert_almost_equal(
        transformer.transform(
            xx=scalar_and_array(3513638.1999428216),
            yy=scalar_and_array(778956.4532640711),
            zz=scalar_and_array(5248216.453456361),
            tt=scalar_and_array(2008.75),
            direction=TransformDirection.INVERSE,
        ),
        (
            scalar_and_array(3513638.19380),
            scalar_and_array(778956.45250),
            scalar_and_array(5248216.46900),
            scalar_and_array(2008.75),
        ),
    )


def test_transform_direction(scalar_and_array):
    forward_transformer = Transformer.from_crs(4326, 3857)
    inverse_transformer = Transformer.from_crs(3857, 4326)
    assert_array_equal(
        inverse_transformer.transform(
            scalar_and_array(-33),
            scalar_and_array(24),
            direction=TransformDirection.INVERSE,
        ),
        forward_transformer.transform(scalar_and_array(-33), scalar_and_array(24)),
    )
    ident_transformer = Transformer.from_crs(4326, 3857)
    assert_array_equal(
        ident_transformer.transform(
            scalar_and_array(-33),
            scalar_and_array(24),
            direction=TransformDirection.IDENT,
        ),
        (scalar_and_array(-33), scalar_and_array(24)),
    )


def test_always_xy__transformer(scalar_and_array):
    transformer = Transformer.from_crs(2193, 4326, always_xy=True)
    assert_almost_equal(
        transformer.transform(scalar_and_array(1625350), scalar_and_array(5504853)),
        (
            scalar_and_array(173.29964730317386),
            scalar_and_array(-40.60674802693758),
        ),
    )


def test_always_xy__transform():
    with pytest.warns(FutureWarning):
        assert_almost_equal(
            transform(2193, 4326, 1625350, 5504853, always_xy=True),
            (173.29964730317386, -40.60674802693758),
        )


def test_always_xy__itransform():
    with pytest.warns(FutureWarning):
        assert_almost_equal(
            list(itransform(2193, 4326, [(1625350, 5504853)], always_xy=True)),
            [(173.29964730317386, -40.60674802693758)],
        )


def test_always_xy__from_pipeline(scalar_and_array):
    # Test that always_xy works with from_pipeline
    # EPSG:1311 is "ED50 to WGS 84 (18)" - a transformation from ED50 to WGS84
    # Without always_xy, input/output order depends on CRS axis order (lat/lon)
    # With always_xy=True, input/output is always lon/lat (x/y)
    transformer_xy = Transformer.from_pipeline("EPSG:1311", always_xy=True)
    transformer_default = Transformer.from_pipeline("EPSG:1311", always_xy=False)

    # Coordinates in Spain (within ED50 to WGS84 (18) area of use)
    lon, lat = -3.7, 40.4  # Madrid area

    # With always_xy=True, input should be (lon, lat) and output is (lon, lat)
    result_xy = transformer_xy.transform(scalar_and_array(lon), scalar_and_array(lat))

    # With always_xy=False, input should be (lat, lon) and output is (lat, lon)
    result_default = transformer_default.transform(
        scalar_and_array(lat), scalar_and_array(lon)
    )

    # Both should produce equivalent results (just in different order)
    # result_xy is (lon_out, lat_out), result_default is (lat_out, lon_out)
    assert_almost_equal(result_xy[0], result_default[1], decimal=6)
    assert_almost_equal(result_xy[1], result_default[0], decimal=6)


def test_always_xy__from_pipeline_vs_transformer_group():
    """
    Test that from_pipeline with always_xy produces the same results
    as TransformerGroup with always_xy for the same coordinate operation.
    """
    # Get transformers from TransformerGroup for ED50 to WGS84
    trans_group = TransformerGroup(
        "EPSG:4230",  # ED50
        "EPSG:4326",  # WGS84
        always_xy=True,
        allow_superseded=True,
    )

    # Find the transformer with "ED50 to WGS 84 (1)" in its description
    # This corresponds to EPSG:1133
    target_description = "ED50 to WGS 84 (1)"
    group_transformer = None
    for transformer in trans_group.transformers:
        if target_description in transformer.description:
            group_transformer = transformer
            break

    assert group_transformer is not None, (
        f"Could not find transformer with '{target_description}' in TransformerGroup"
    )

    # Create the same transformation using from_pipeline with always_xy
    pipeline_transformer = Transformer.from_pipeline("EPSG:1133", always_xy=True)

    # Test coordinates
    test_coords = [
        (-3.7, 40.4),
        (-9.1, 38.7),
        (2.2, 41.4),
    ]

    for lon, lat in test_coords:
        result_group = group_transformer.transform(lon, lat)
        result_pipeline = pipeline_transformer.transform(lon, lat)

        assert_almost_equal(
            result_group,
            result_pipeline,
            decimal=9,
            err_msg=f"Results differ for coordinates ({lon}, {lat})",
        )


def test_always_xy__from_pipeline_axis_order():
    """
    Test that always_xy properly normalizes axis order for from_pipeline.
    Verifies that with always_xy=True, input/output is (lon, lat) order,
    and with always_xy=False, it follows the CRS native axis order.
    """
    # EPSG:1133 is "ED50 to WGS 84 (1)" - transforms from ED50 to WGS84
    # Both CRS have native axis order of (lat, lon)

    transformer_xy = Transformer.from_pipeline("EPSG:1133", always_xy=True)
    transformer_native = Transformer.from_pipeline("EPSG:1133", always_xy=False)

    # Test point in Denmark (within EPSG:1133 area of use)
    lon, lat = 10.0, 55.0

    # With always_xy=True: input (lon, lat) -> output (lon, lat)
    result_xy = transformer_xy.transform(lon, lat)

    # With always_xy=False: input (lat, lon) -> output (lat, lon)
    result_native = transformer_native.transform(lat, lon)

    # Results should be equivalent but in different order
    # result_xy = (lon_out, lat_out)
    # result_native = (lat_out, lon_out)
    assert_almost_equal(result_xy[0], result_native[1], decimal=9)
    assert_almost_equal(result_xy[1], result_native[0], decimal=9)

    # Also verify the transformation actually changes the coordinates
    # (i.e., it's not a null transformation)
    assert result_xy[0] != lon or result_xy[1] != lat


def test_always_xy__from_pipeline_projected_crs():
    """
    Test that always_xy works with from_pipeline for projected CRS transformations.
    Uses TransformerGroup to find a transformation between projected CRS and
    compares with from_pipeline using the same operation code.
    """
    # Get transformers from TransformerGroup for ED50 / UTM zone 31N to WGS84
    # EPSG:23031 (ED50 / UTM zone 31N) -> EPSG:4326 (WGS 84)
    trans_group = TransformerGroup(
        "EPSG:23031",  # ED50 / UTM zone 31N
        "EPSG:4326",  # WGS 84
        always_xy=True,
        allow_superseded=True,
    )

    # Find a transformer with "ED50 to WGS 84 (1)" in its description
    target_description = "ED50 to WGS 84 (1)"
    group_transformer = None
    for transformer in trans_group.transformers:
        if target_description in transformer.description:
            group_transformer = transformer
            break

    assert group_transformer is not None, (
        f"Could not find transformer with '{target_description}' in TransformerGroup. "
        f"Available: {[t.description for t in trans_group.transformers[:5]]}"
    )

    # Test UTM coordinates (ED50 / UTM zone 31N)
    # With always_xy=True, input is (easting, northing), output is (lon, lat)
    test_coords = [
        (430000, 4580000),
        (450000, 4600000),
        (400000, 4550000),
    ]

    for easting, northing in test_coords:
        result = group_transformer.transform(easting, northing)
        lon_out, lat_out = result

        # Verify the result is in geographic coordinates (reasonable lon/lat range)
        assert -180 <= lon_out <= 180, f"Longitude {lon_out} out of range"
        assert -90 <= lat_out <= 90, f"Latitude {lat_out} out of range"

        # Should be roughly in Catalonia/Barcelona area
        assert 0 < lon_out < 4, f"Longitude {lon_out} not in expected range"
        assert 40 < lat_out < 43, f"Latitude {lat_out} not in expected range"


def test_always_xy__from_pipeline_projected_to_geographic():
    """
    Test from_pipeline with always_xy for transformation
    from projected to geographic CRS. Uses TransformerGroup
    to compare results.
    """
    # Get transformer from TransformerGroup for projected -> geographic
    # EPSG:23031 (ED50 / UTM zone 31N) -> EPSG:4326 (WGS 84)
    trans_group_xy = TransformerGroup("EPSG:23031", "EPSG:4326", always_xy=True)
    trans_group_native = TransformerGroup("EPSG:23031", "EPSG:4326", always_xy=False)

    transformer_xy = trans_group_xy.transformers[0]
    transformer_native = trans_group_native.transformers[0]

    # Test UTM coordinates (Barcelona area in ED50 / UTM zone 31N)
    easting, northing = 430000, 4580000

    # With always_xy=True: input (easting, northing) -> output (lon, lat)
    result_xy = transformer_xy.transform(easting, northing)

    # With always_xy=False: input (easting, northing), output (lat, lon)
    result_native = transformer_native.transform(easting, northing)

    # result_xy is (lon, lat), result_native is (lat, lon)
    lon_out, lat_out = result_xy
    lat_native, lon_native = result_native

    # Verify results match (just different output order)
    assert_almost_equal(lon_out, lon_native, decimal=9)
    assert_almost_equal(lat_out, lat_native, decimal=9)

    # Verify the result is in geographic coordinates (reasonable lon/lat range)
    assert -180 <= lon_out <= 180, f"Longitude {lon_out} out of range"
    assert -90 <= lat_out <= 90, f"Latitude {lat_out} out of range"

    # Should be roughly in Catalonia/Barcelona area
    assert 0 < lon_out < 4, f"Longitude {lon_out} not in expected range for Barcelona"
    assert 40 < lat_out < 43, f"Latitude {lat_out} not in expected range for Barcelona"


def test_always_xy__from_pipeline_geographic_to_projected():
    """
    Test from_pipeline with always_xy for transformation
    from geographic to projected CRS. Uses TransformerGroup
    to compare results.

    WGS84 (EPSG:4326) has native axis order (lat, lon)
    UTM zone 31N (EPSG:32631) has native axis order (E, N) which is already x, y

    With always_xy=True: input (lon, lat) -> output (easting, northing)
    With always_xy=False: input (lat, lon) -> output (easting, northing)
    """
    # Get transformer from TransformerGroup for geographic -> projected
    # EPSG:4326 (WGS 84) -> EPSG:32631 (WGS 84 / UTM zone 31N)
    trans_group_xy = TransformerGroup(
        "EPSG:4326",
        "EPSG:32631",
        always_xy=True,
    )
    trans_group_native = TransformerGroup(
        "EPSG:4326",
        "EPSG:32631",
        always_xy=False,
    )

    transformer_xy = trans_group_xy.transformers[0]
    transformer_native = trans_group_native.transformers[0]

    # Test geographic coordinates (Barcelona area)
    lon, lat = 2.17, 41.39  # Barcelona

    # With always_xy=True: input (lon, lat) -> output (easting, northing)
    result_xy = transformer_xy.transform(lon, lat)

    # With always_xy=False: input (lat, lon) -> output (easting, northing)
    # Note: UTM already has (E, N) order which is x, y, so output order is same
    result_native = transformer_native.transform(lat, lon)

    # Both should produce (easting, northing)
    easting_xy, northing_xy = result_xy
    easting_native, northing_native = result_native

    # Verify results match
    assert_almost_equal(easting_xy, easting_native, decimal=6)
    assert_almost_equal(northing_xy, northing_native, decimal=6)

    # Verify the result is in UTM range (reasonable easting/northing)
    assert 100000 < easting_xy < 900000, f"Easting {easting_xy} out of UTM range"
    assert 0 < northing_xy < 10000000, f"Northing {northing_xy} out of UTM range"


@pytest.mark.parametrize("empty_array", [(), [], numpy.array([])])
def test_transform_empty_array_xy(empty_array):
    transformer = Transformer.from_crs(2193, 4326)
    assert_array_equal(
        transformer.transform(empty_array, empty_array), (empty_array, empty_array)
    )


@pytest.mark.parametrize("empty_array", [(), [], numpy.array([])])
def test_transform_empty_array_xyzt(empty_array):
    transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000")
    assert_array_equal(
        transformer.transform(empty_array, empty_array, empty_array, empty_array),
        (empty_array, empty_array, empty_array, empty_array),
    )


def test_transform_direction__string(scalar_and_array):
    forward_transformer = Transformer.from_crs(4326, 3857)
    inverse_transformer = Transformer.from_crs(3857, 4326)
    assert_array_equal(
        inverse_transformer.transform(
            scalar_and_array(-33), scalar_and_array(24), direction="INVERSE"
        ),
        forward_transformer.transform(
            scalar_and_array(-33), scalar_and_array(24), direction="FORWARD"
        ),
    )
    ident_transformer = Transformer.from_crs(4326, 3857)
    assert_array_equal(
        ident_transformer.transform(
            scalar_and_array(-33), scalar_and_array(24), direction="IDENT"
        ),
        (scalar_and_array(-33), scalar_and_array(24)),
    )


def test_transform_direction__string_lowercase(scalar_and_array):
    forward_transformer = Transformer.from_crs(4326, 3857)
    inverse_transformer = Transformer.from_crs(3857, 4326)
    assert_array_equal(
        inverse_transformer.transform(
            scalar_and_array(-33), scalar_and_array(24), direction="inverse"
        ),
        forward_transformer.transform(
            scalar_and_array(-33), scalar_and_array(24), direction="forward"
        ),
    )
    ident_transformer = Transformer.from_crs(4326, 3857)
    assert_array_equal(
        ident_transformer.transform(
            scalar_and_array(-33), scalar_and_array(24), direction="ident"
        ),
        (scalar_and_array(-33), scalar_and_array(24)),
    )


def test_transform_direction__invalid(scalar_and_array):
    transformer = Transformer.from_crs(4326, 3857)
    with pytest.raises(ValueError, match="Invalid value"):
        transformer.transform(
            scalar_and_array(-33), scalar_and_array(24), direction="WHEREVER"
        )


def test_from_pipeline__non_transform_input():
    with pytest.raises(ProjError, match="Input is not a transformation"):
        Transformer.from_pipeline("epsg:4326")


def test_non_supported_initialization():
    with pytest.raises(ProjError, match="Transformer must be initialized using"):
        Transformer()


def test_pj_info_properties():
    transformer = Transformer.from_crs(4326, 3857)
    assert transformer.name == "pipeline"
    assert transformer.description == "Popular Visualisation Pseudo-Mercator"
    assert transformer.definition.startswith("proj=pipeline")
    assert transformer.has_inverse
    assert transformer.accuracy == 0


def test_to_proj4():
    transformer = Transformer.from_crs(4326, 3857)
    proj_string = transformer.to_proj4()
    assert "+proj=pipeline" in proj_string
    assert "\n" not in proj_string


def test_to_proj4__pretty():
    transformer = Transformer.from_crs(4326, 3857)
    proj_string = transformer.to_proj4(pretty=True)
    assert "+proj=pipeline" in proj_string
    assert "\n" in proj_string


def test_to_wkt():
    transformer = Transformer.from_crs(4326, 3857)
    assert transformer.to_wkt().startswith(
        'CONVERSION["Popular Visualisation Pseudo-Mercator"'
    )


def test_str():
    assert str(Transformer.from_crs(4326, 3857)).startswith("proj=pipeline")


def test_repr():
    assert repr(Transformer.from_crs(4326, 3857)) == (
        "<Conversion Transformer: pipeline>\n"
        "Description: Popular Visualisation Pseudo-Mercator\n"
        "Area of Use:\n"
        "- name: World.\n"
        "- bounds: (-180.0, -90.0, 180.0, 90.0)"
    )


@pytest.mark.grid
def test_repr__conditional():
    trans_repr = repr(Transformer.from_crs("EPSG:4326+3855", "EPSG:4979"))
    if grids_available("us_nga_egm08_25.tif"):
        assert trans_repr == (
            "<Unknown Transformer: unknown>\n"
            "Description: unavailable until proj_trans is called\n"
            "Area of Use:\n- undefined"
        )
    else:
        assert trans_repr == (
            "<Other Coordinate Operation Transformer: noop>\n"
            "Description: Transformation from EGM2008 height to WGS 84 "
            "(ballpark vertical transformation, without ellipsoid height "
            "to vertical height correction)\n"
            "Area of Use:\n"
            "- name: World.\n"
            "- bounds: (-180.0, -90.0, 180.0, 90.0)"
        )


def test_to_json_dict():
    transformer = Transformer.from_crs(4326, 3857)
    json_dict = transformer.to_json_dict()
    assert json_dict["type"] == "Conversion"


def test_to_json():
    transformer = Transformer.from_crs(4326, 3857)
    json_data = transformer.to_json()
    assert "Conversion" in json_data
    assert "\n" not in json_data


def test_to_json__pretty():
    transformer = Transformer.from_crs(4326, 3857)
    json_data = transformer.to_json(pretty=True)
    assert "Conversion" in json_data
    assert json_data.startswith('{\n  "')


def test_to_json__pretty__indenation():
    transformer = Transformer.from_crs(4326, 3857)
    json_data = transformer.to_json(pretty=True, indentation=4)
    assert "Conversion" in json_data
    assert json_data.startswith('{\n    "')


def test_transformer__operations():
    transformer = TransformerGroup(28356, 7856).transformers[0]
    assert [op.name for op in transformer.operations] == [
        "Inverse of Map Grid of Australia zone 56",
        "GDA94 to GDA2020 (1)",
        "Map Grid of Australia zone 56",
    ]


def test_transformer__operations_missing():
    assert Transformer.from_crs(7789, 8401).operations == ()


def test_transformer__operations__scope_remarks():
    transformer = TransformerGroup(28356, 7856).transformers[0]
    assert transformer.scope is None
    assert [op.scope for op in transformer.operations] == [
        "Engineering survey, topographic mapping.",
        "Transformation of GDA94 coordinates that have been derived through GNSS CORS.",
        "Engineering survey, topographic mapping.",
    ]
    assert [str(op.remarks)[:5].strip() for op in transformer.operations] == [
        "Grid",
        "Scale",
        "Grid",
    ]


@pytest.mark.grid
def test_transformer__only_best():
    transformer = Transformer.from_crs(4326, 2964, only_best=True)
    if not grids_available("ca_nrc_ntv2_0.tif"):
        with pytest.raises(
            ProjError,
            match=r"Grid ca_nrc_ntv2_0.tif is not available.",
        ):
            transformer.transform(60, -100, errcheck=True)


def test_transformer_group():
    trans_group = TransformerGroup(7789, 8401)
    assert len(trans_group.transformers) == 2
    assert trans_group.transformers[0].name == "helmert"
    assert trans_group.transformers[1].description == ("ITRF2014 to ETRF2014 (1)")
    assert not trans_group.unavailable_operations
    assert trans_group.best_available


@pytest.mark.grid
def test_transformer_group__unavailable():
    trans_group = TransformerGroup(4326, 2964)
    for transformer in trans_group.transformers:
        assert transformer.is_network_enabled == (
            os.environ.get("PROJ_NETWORK") == "ON"
        )

    if grids_available("us_noaa_alaska.tif", "ca_nrc_ntv2_0.tif", check_all=True):
        assert len(trans_group.unavailable_operations) == 0
        assert len(trans_group.transformers) == 10
        assert (
            trans_group.transformers[0].description
            == "Inverse of NAD27 to WGS 84 (85) + Alaska Albers"
        )
        assert trans_group.best_available
    elif grids_available("us_noaa_alaska.tif"):
        assert len(trans_group.unavailable_operations) == 1
        assert (
            trans_group.transformers[0].description
            == "Inverse of NAD27 to WGS 84 (85) + Alaska Albers"
        )
        assert len(trans_group.transformers) == 9
        assert trans_group.best_available
    elif grids_available("ca_nrc_ntv2_0.tif"):
        assert len(trans_group.unavailable_operations) == 1
        assert (
            trans_group.transformers[0].description
            == "Inverse of NAD27 to WGS 84 (7) + Alaska Albers"
        )
        assert len(trans_group.transformers) == 9
        assert not trans_group.best_available
    else:
        assert len(trans_group.unavailable_operations) == 2
        assert (
            trans_group.unavailable_operations[0].name
            == "Inverse of NAD27 to WGS 84 (85) + Alaska Albers"
        )
        assert len(trans_group.transformers) == 8
        assert not trans_group.best_available


@pytest.mark.grid
def test_transform_group__missing_best():
    with pytest.warns(FutureWarning):
        lat_lon_proj = pyproj.Proj(init="epsg:4326", preserve_units=False)
        alaska_aea_proj = pyproj.Proj(init="epsg:2964", preserve_units=False)

    if not grids_available("ca_nrc_ntv2_0.tif"):
        with pytest.warns(
            UserWarning,
            match="Best transformation is not available due to missing Grid",
        ):
            trans_group = pyproj.transformer.TransformerGroup(
                lat_lon_proj.crs, alaska_aea_proj.crs
            )

        assert not trans_group.best_available
        assert "ntv2_0" not in trans_group.transformers[0].definition
        assert "ntv2_0" in trans_group.unavailable_operations[0].to_proj4()
    else:
        # assuming all grids available or PROJ_NETWORK=ON
        trans_group = pyproj.transformer.TransformerGroup(
            lat_lon_proj.crs, alaska_aea_proj.crs
        )
        assert trans_group.best_available
        assert "ntv2_0" in trans_group.transformers[0].definition


@pytest.mark.grid
def test_transform_group__area_of_interest():
    def get_transformer_group():
        return pyproj.transformer.TransformerGroup(
            4326,
            2964,
            area_of_interest=pyproj.transformer.AreaOfInterest(
                -136.46, 49.0, -60.72, 83.17
            ),
        )

    if not grids_available("ca_nrc_ntv2_0.tif"):
        with pytest.warns(
            UserWarning,
            match="Best transformation is not available due to missing Grid",
        ):
            trans_group = get_transformer_group()
        assert (
            trans_group.transformers[0].description
            == "Inverse of NAD27 to WGS 84 (13) + Alaska Albers"
        )
    else:
        trans_group = get_transformer_group()
        assert trans_group.best_available
        assert (
            trans_group.transformers[0].description
            == "Inverse of NAD27 to WGS 84 (33) + Alaska Albers"
        )


@pytest.mark.grid
def test_transformer_group__get_transform_crs():
    tg = TransformerGroup("epsg:4258", "epsg:7415")
    if grids_available(
        "nl_nsgi_nlgeo2018.tif", "nl_nsgi_rdtrans2018.tif", check_all=True
    ):
        assert len(tg.transformers) == 2
    else:
        assert len(tg.transformers) == 1


def test_transformer__area_of_interest():
    transformer = Transformer.from_crs(
        "EPSG:7789",
        "EPSG:4936",
        area_of_interest=AreaOfInterest(-177.25, -44.64, -175.54, -43.3),
    )
    assert (
        transformer.description
        == "Ballpark geocentric translation from ITRF2014 to ETRS89"
    )


def test_transformer_proj__area_of_interest():
    transformer = Transformer.from_proj(
        "EPSG:7789",
        "EPSG:4936",
        area_of_interest=AreaOfInterest(-177.25, -44.64, -175.54, -43.3),
    )
    assert (
        transformer.description
        == "Ballpark geocentric translation from ITRF2014 to ETRS89"
    )


def test_transformer__area_of_interest__invalid():
    with pytest.raises(ProjError):
        Transformer.from_crs(
            4326, 2964, area_of_interest=(-136.46, 49.0, -60.72, 83.17)
        )


def test_transformer_group__area_of_interest__invalid():
    with pytest.raises(ProjError):
        TransformerGroup(4326, 2964, area_of_interest=(-136.46, 49.0, -60.72, 83.17))


def test_transformer_equals():
    assert (
        TransformerGroup(28356, 7856).transformers[0]
        == TransformerGroup(28356, 7856).transformers[0]
    )


@pytest.mark.parametrize(
    "comparison",
    [Transformer.from_pipeline("+proj=pipeline +ellps=GRS80 +step +proj=cart"), 22],
)
def test_transformer_not_equals(comparison):
    assert Transformer.from_crs(28356, 7856) != comparison


@pytest.mark.parametrize(
    "pipeline_str",
    [
        "+proj=pipeline +ellps=GRS80 +step +proj=cart",
        "+proj=pipeline +step +proj=unitconvert +xy_in=deg "
        "+xy_out=rad +ellps=GRS80 +step +proj=cart",
    ],
)
def test_pipeline_transform(pipeline_str):
    trans = Transformer.from_pipeline(pipeline_str)
    assert_almost_equal(
        trans.transform(50, 25, 0),
        (3717892.6072086394, 4430811.87152035, 2679074.4628772778),
    )


@pytest.mark.parametrize(
    "pipeline_str",
    [
        "+proj=pipeline +ellps=GRS80 +step +proj=cart",
        "+proj=pipeline +step +proj=unitconvert +xy_in=deg "
        "+xy_out=rad +ellps=GRS80 +step +proj=cart",
    ],
)
def test_pipeline_itransform(pipeline_str):
    trans = Transformer.from_pipeline(pipeline_str)
    assert_almost_equal(
        list(trans.itransform([(50, 25, 0)])),
        [(3717892.6072086394, 4430811.87152035, 2679074.4628772778)],
    )


@pytest.mark.parametrize(
    "transformer",
    [
        partial(
            Transformer.from_pipeline, "+proj=pipeline +ellps=GRS80 +step +proj=cart"
        ),
        partial(Transformer.from_crs, 4326, 3857),
        partial(Transformer.from_proj, 4326, 3857),
    ],
)
@patch.dict("os.environ", {"PROJ_NETWORK": "ON"}, clear=True)
def test_network__disable(transformer):
    with proj_network_env():
        pyproj.network.set_network_enabled(active=False)
        trans = transformer()
        assert trans.is_network_enabled is False


@pytest.mark.parametrize(
    "transformer",
    [
        partial(
            Transformer.from_pipeline, "+proj=pipeline +ellps=GRS80 +step +proj=cart"
        ),
        partial(Transformer.from_crs, 4326, 3857),
        partial(Transformer.from_proj, 4326, 3857),
    ],
)
@patch.dict("os.environ", {"PROJ_NETWORK": "OFF"}, clear=True)
def test_network__enable(transformer):
    with proj_network_env():
        pyproj.network.set_network_enabled(active=True)
        trans = transformer()
        assert trans.is_network_enabled is True


@pytest.mark.parametrize(
    "transformer",
    [
        partial(
            Transformer.from_pipeline, "+proj=pipeline +ellps=GRS80 +step +proj=cart"
        ),
        partial(Transformer.from_crs, 4326, 3857),
        partial(Transformer.from_proj, 4326, 3857),
    ],
)
def test_network__default(transformer):
    with proj_network_env():
        pyproj.network.set_network_enabled()
        trans = transformer()
        assert trans.is_network_enabled == (os.environ.get("PROJ_NETWORK") == "ON")


@patch.dict("os.environ", {"PROJ_NETWORK": "OFF"}, clear=True)
def test_transformer_group__network_enabled():
    with proj_network_env():
        pyproj.network.set_network_enabled(active=True)
        trans_group = TransformerGroup(4326, 2964)
        assert len(trans_group.unavailable_operations) == 0
        assert len(trans_group.transformers) == 10
        assert trans_group.best_available
        for transformer in trans_group.transformers:
            assert transformer.is_network_enabled is True
            for operation in transformer.operations:
                for grid in operation.grids:
                    assert grid.available


@pytest.mark.grid
@patch.dict("os.environ", {"PROJ_NETWORK": "ON"}, clear=True)
def test_transformer_group__network_disabled():
    with proj_network_env():
        pyproj.network.set_network_enabled(active=False)
        trans_group = TransformerGroup(4326, 2964)
        for transformer in trans_group.transformers:
            assert transformer.is_network_enabled is False

        if grids_available(
            "us_noaa_alaska.tif",
            "ca_nrc_ntv2_0.tif",
            check_network=False,
            check_all=True,
        ):
            assert len(trans_group.unavailable_operations) == 0
            assert len(trans_group.transformers) == 10
            assert (
                trans_group.transformers[0].description
                == "Inverse of NAD27 to WGS 84 (85) + Alaska Albers"
            )
            assert trans_group.best_available
        elif grids_available("us_noaa_alaska.tif", check_network=False):
            assert len(trans_group.unavailable_operations) == 1
            assert (
                trans_group.transformers[0].description
                == "Inverse of NAD27 to WGS 84 (85) + Alaska Albers"
            )
            assert len(trans_group.transformers) == 9
            assert trans_group.best_available
        elif grids_available("ca_nrc_ntv2_0.tif", check_network=False):
            assert len(trans_group.unavailable_operations) == 1
            assert (
                trans_group.transformers[0].description
                == "Inverse of NAD27 to WGS 84 (7) + Alaska Albers"
            )
            assert len(trans_group.transformers) == 9
            assert not trans_group.best_available
        else:
            assert len(trans_group.unavailable_operations) == 2
            assert (
                trans_group.unavailable_operations[0].name
                == "Inverse of NAD27 to WGS 84 (85) + Alaska Albers"
            )
            assert len(trans_group.transformers) == 8
            assert not trans_group.best_available


def test_transform_pipeline_radians():
    trans = Transformer.from_pipeline(
        "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 "
        "+step +proj=unitconvert +xy_in=rad +xy_out=deg"
    )
    assert_almost_equal(
        trans.transform(-2704026.010, -4253051.810, 3895878.820, radians=True),
        (-2.137113493845668, 0.6613203738996222, -20.531156923621893),
    )

    assert_almost_equal(
        trans.transform(
            -2.137113493845668,
            0.6613203738996222,
            -20.531156923621893,
            radians=True,
            direction=TransformDirection.INVERSE,
        ),
        (-2704026.010, -4253051.810, 3895878.820),
    )


def test_itransform_pipeline_radians():
    trans = Transformer.from_pipeline(
        "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 "
        "+step +proj=unitconvert +xy_in=rad +xy_out=deg"
    )
    assert_almost_equal(
        list(
            trans.itransform([(-2704026.010, -4253051.810, 3895878.820)], radians=True)
        ),
        [(-2.137113493845668, 0.6613203738996222, -20.531156923621893)],
    )

    assert_almost_equal(
        list(
            trans.itransform(
                [(-2.137113493845668, 0.6613203738996222, -20.531156923621893)],
                radians=True,
                direction=TransformDirection.INVERSE,
            )
        ),
        [(-2704026.010, -4253051.810, 3895878.820)],
    )


@pytest.mark.parametrize("x,y,z", permutations([10, [10], (10,)]))  # 6 test cases
def test_transform_honours_input_types(x, y, z):
    # 622
    transformer = Transformer.from_proj(4896, 4896)
    assert transformer.transform(xx=x, yy=y, zz=z) == (x, y, z)


@pytest.mark.grid
@pytest.mark.network
@patch("pyproj.transformer.get_user_data_dir")
def test_transformer_group__download_grids(get_user_data_dir_mock, tmp_path, capsys):
    get_user_data_dir_mock.return_value = str(tmp_path)
    with proj_network_env():
        pyproj.network.set_network_enabled(active=False)
        trans_group = TransformerGroup(4326, 2964)
        trans_group.download_grids(verbose=True)
        captured = capsys.readouterr()
        get_user_data_dir_mock.assert_called_with(True)
        paths = sorted(Path(path).name for path in glob(str(tmp_path.joinpath("*"))))
        if grids_available(
            "us_noaa_alaska.tif",
            "ca_nrc_ntv2_0.tif",
            check_network=False,
            check_all=True,
        ):
            assert paths == []
            assert captured.out == ""
        elif grids_available("us_noaa_alaska.tif", check_network=False):
            assert paths == ["ca_nrc_ntv2_0.tif"]
            assert (
                captured.out == "Downloading: https://cdn.proj.org/ca_nrc_ntv2_0.tif\n"
            )
        elif grids_available("ca_nrc_ntv2_0.tif", check_network=False):
            assert paths == ["us_noaa_alaska.tif"]
            assert captured.out == (
                "Downloading: https://cdn.proj.org/us_noaa_alaska.tif\n"
            )
        else:
            assert paths == ["ca_nrc_ntv2_0.tif", "us_noaa_alaska.tif"]
            assert captured.out == (
                "Downloading: https://cdn.proj.org/us_noaa_alaska.tif\n"
                "Downloading: https://cdn.proj.org/ca_nrc_ntv2_0.tif\n"
            )
        # make sure not downloaded again
        with (
            proj_env(),
            patch("pyproj.transformer._download_resource_file") as download_mock,
        ):
            append_data_dir(str(tmp_path))
            trans_group = TransformerGroup(4326, 2964)
            trans_group.download_grids()
            get_user_data_dir_mock.assert_called_with(True)
            download_mock.assert_not_called()


@pytest.mark.grid
@patch("pyproj.transformer._download_resource_file")
@patch("pyproj.transformer.get_user_data_dir")
def test_transformer_group__download_grids__directory(
    get_user_data_dir_mock, download_mock, tmp_path, capsys
):
    with proj_network_env():
        pyproj.network.set_network_enabled(active=False)
        trans_group = TransformerGroup(4326, 2964)
        trans_group.download_grids(directory=tmp_path)
        get_user_data_dir_mock.assert_not_called()
        captured = capsys.readouterr()
        assert captured.out == ""
        if grids_available(
            "us_noaa_alaska.tif",
            "ca_nrc_ntv2_0.tif",
            check_network=False,
            check_all=True,
        ):
            download_mock.assert_not_called()
        elif grids_available("us_noaa_alaska.tif", check_network=False):
            download_mock.assert_called_with(
                file_url="https://cdn.proj.org/ca_nrc_ntv2_0.tif",
                short_name="ca_nrc_ntv2_0.tif",
                directory=tmp_path,
                verbose=False,
            )
        elif grids_available("ca_nrc_ntv2_0.tif", check_network=False):
            download_mock.assert_called_with(
                file_url="https://cdn.proj.org/us_noaa_alaska.tif",
                short_name="us_noaa_alaska.tif",
                directory=tmp_path,
                verbose=False,
            )
        else:
            download_mock.assert_has_calls(
                [
                    call(
                        file_url="https://cdn.proj.org/us_noaa_alaska.tif",
                        short_name="us_noaa_alaska.tif",
                        directory=tmp_path,
                        verbose=False,
                    ),
                    call(
                        file_url="https://cdn.proj.org/ca_nrc_ntv2_0.tif",
                        short_name="ca_nrc_ntv2_0.tif",
                        directory=tmp_path,
                        verbose=False,
                    ),
                ],
                any_order=True,
            )


def test_transformer_multithread__pipeline():
    # https://github.com/pyproj4/pyproj/issues/782
    trans = Transformer.from_pipeline(
        "+proj=pipeline +step +inv +proj=cart +ellps=WGS84 "
        "+step +proj=unitconvert +xy_in=rad +xy_out=deg"
    )

    def transform(num):
        return trans.transform(-2704026.010, -4253051.810, 3895878.820)

    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
        for result in executor.map(transform, range(10)):
            pass


def test_transformer_multithread__crs():
    # https://github.com/pyproj4/pyproj/issues/782
    trans = Transformer.from_crs(4326, 3857)

    def transform(num):
        return trans.transform(1, 2)

    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
        for result in executor.map(transform, range(10)):
            pass


def test_transformer_accuracy_filter():
    with pytest.raises(ProjError):
        Transformer.from_crs("EPSG:4326", "EPSG:4258", accuracy=0.05)


def test_transformer_allow_ballpark_filter():
    if PROJ_GTE_990:
        transformer = Transformer.from_crs(
            "EPSG:4326", "EPSG:4258", authority="PROJ", allow_ballpark=False
        )
        assert "Ballpark" not in transformer.description
    else:
        with pytest.raises(ProjError):
            Transformer.from_crs(
                "EPSG:4326", "EPSG:4258", authority="PROJ", allow_ballpark=False
            )


def test_transformer_authority_filter():
    transformer = Transformer.from_crs("EPSG:4326", "EPSG:4258", authority="PROJ")
    if PROJ_GTE_990:
        assert transformer.description in {
            "unavailable until proj_trans is called",
            "Ballpark geographic offset from WGS 84 to ETRS89",
        }
    else:
        assert (
            transformer.description
            == "Ballpark geographic offset from WGS 84 to ETRS89"
        )


@pytest.mark.parametrize(
    "input_string",
    [
        "EPSG:1671",
        "urn:ogc:def:coordinateOperation:EPSG::1671",
    ],
)
def test_transformer_from_pipeline__input_types(input_string):
    # PROJ 9.7.1+ renamed this operation from "RGF93 v1 to WGS 84 (1)"
    # to "ETRS89-FRA [RGF93 v1] to WGS 84 (1)"
    if PROJ_GTE_990:
        assert (
            Transformer.from_pipeline(input_string).description
            == "ETRS89-FRA [RGF93 v1] to WGS 84 (1)"
        )
    else:
        assert (
            Transformer.from_pipeline(input_string).description
            == "RGF93 v1 to WGS 84 (1)"
        )


@pytest.mark.parametrize(
    "method_name",
    [
        "to_wkt",
        "to_json",
    ],
)
def test_transformer_from_pipeline__wkt_json(method_name):
    # PROJ 9.7.1+ renamed this operation from "RGF93 v1 to WGS 84 (1)"
    # to "ETRS89-FRA [RGF93 v1] to WGS 84 (1)"
    description = Transformer.from_pipeline(
        getattr(
            Transformer.from_pipeline("urn:ogc:def:coordinateOperation:EPSG::1671"),
            method_name,
        )()
    ).description
    if PROJ_GTE_990:
        assert description == "ETRS89-FRA [RGF93 v1] to WGS 84 (1)"
    else:
        assert description == "RGF93 v1 to WGS 84 (1)"


@pytest.mark.parametrize(
    "density,expected",
    [
        (0, (-1684649.41338, -350356.81377, 1684649.41338, 2234551.18559)),
        (100, (-1684649.41338, -555777.79210, 1684649.41338, 2234551.18559)),
    ],
)
def test_transform_bounds_densify(density, expected):
    transformer = Transformer.from_crs(
        "EPSG:4326",
        "+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 "
        "+a=6370997 +b=6370997 +units=m +no_defs",
    )
    assert numpy.allclose(
        transformer.transform_bounds(40, -120, 64, -80, densify_pts=density),
        expected,
    )


@pytest.mark.parametrize(
    "density,expected",
    [
        (0, (-1684649.41338, -350356.81377, 1684649.41338, 2234551.18559)),
        (100, (-1684649.41338, -555777.79210, 1684649.41338, 2234551.18559)),
    ],
)
@pytest.mark.parametrize(
    "input_bounds, radians",
    [
        ((-120, 40, -80, 64), False),
        (
            (
                numpy.radians(-120),
                numpy.radians(40),
                numpy.radians(-80),
                numpy.radians(64),
            ),
            True,
        ),
    ],
)
def test_transform_bounds_densify__xy(density, expected, input_bounds, radians):
    transformer = Transformer.from_crs(
        "EPSG:4326",
        "+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 "
        "+a=6370997 +b=6370997 +units=m +no_defs",
        always_xy=True,
    )
    assert numpy.allclose(
        transformer.transform_bounds(
            *input_bounds, densify_pts=density, radians=radians
        ),
        expected,
    )


def test_transform_bounds_densify_out_of_bounds():
    transformer = Transformer.from_crs(
        "EPSG:4326",
        "+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 "
        "+a=6370997 +b=6370997 +units=m +no_defs",
        always_xy=True,
    )
    with pytest.raises(ProjError):
        transformer.transform_bounds(-120, 40, -80, 64, densify_pts=-1)


def test_transform_bounds_densify_out_of_bounds__geographic_output():
    transformer = Transformer.from_crs(
        "+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 "
        "+a=6370997 +b=6370997 +units=m +no_defs",
        "EPSG:4326",
        always_xy=True,
    )
    with pytest.raises(ProjError):
        transformer.transform_bounds(-120, 40, -80, 64, densify_pts=1)


def test_transform_bounds_radians_output():
    transformer = Transformer.from_crs(
        "EPSG:4326",
        "+proj=geocent +ellps=WGS84 +datum=WGS84",
        always_xy=True,
    )
    assert_almost_equal(
        transformer.transform_bounds(
            -2704026.010,
            -4253051.810,
            -2704025.010,
            -4253050.810,
            radians=True,
            direction="INVERSE",
        ),
        (-2.1371136, 0.0, -2.1371133, 0.0),
    )


def test_transform_bounds__antimeridian():
    crs = CRS("EPSG:3851")
    transformer = Transformer.from_crs(crs.geodetic_crs, crs)
    minx, miny, maxx, maxy = crs.area_of_use.bounds
    transformed_bounds = transformer.transform_bounds(miny, minx, maxy, maxx)
    assert_almost_equal(
        transformed_bounds,
        (5228058.6143420935, 1722483.900174921, 8692574.544944234, 4624385.494808555),
    )
    assert_almost_equal(
        transformer.transform_bounds(
            *transformed_bounds,
            direction="INVERSE",
        ),
        (-56.7471249, 153.2799922, -24.6148194, -162.1813873),
    )


def test_transform_bounds__antimeridian__xy():
    crs = CRS("EPSG:3851")
    transformer = Transformer.from_crs(
        crs.geodetic_crs,
        crs,
        always_xy=True,
    )
    transformed_bounds = transformer.transform_bounds(*crs.area_of_use.bounds)
    assert_almost_equal(
        transformed_bounds,
        (1722483.900174921, 5228058.6143420935, 4624385.494808555, 8692574.544944234),
    )
    assert_almost_equal(
        transformer.transform_bounds(*transformed_bounds, direction="INVERSE"),
        (153.2799922, -56.7471249, -162.1813873, -24.6148194),
    )


def test_transform_bounds__beyond_global_bounds():
    transformer = Transformer.from_crs(
        "EPSG:6933",
        "EPSG:4326",
        always_xy=True,
    )
    assert_almost_equal(
        transformer.transform_bounds(
            -17367531.3203125, -7314541.19921875, 17367531.3203125, 7314541.19921875
        ),
        (-180, -85.0445994113099, 180, 85.0445994113099),
    )


def test_transform_bounds__ignore_inf_geographic():
    crs_wkt = (
        'PROJCS["Interrupted_Goode_Homolosine",'
        'GEOGCS["GCS_unnamed ellipse",DATUM["D_unknown",'
        'SPHEROID["Unknown",6378137,298.257223563]],'
        'PRIMEM["Greenwich",0],UNIT["Degree",0.0174532925199433]],'
        'PROJECTION["Interrupted_Goode_Homolosine"],'
        'UNIT["metre",1,AUTHORITY["EPSG","9001"]],'
        'AXIS["Easting",EAST],AXIS["Northing",NORTH]]'
    )
    transformer = Transformer.from_crs(crs_wkt, "EPSG:4326", always_xy=True)
    assert_almost_equal(
        transformer.transform_bounds(
            left=-15028000.0, bottom=7515000.0, right=-14975000.0, top=7556000.0
        ),
        (-179.2133, 70.9345, -177.9054, 71.4364),
        decimal=0,
    )


def test_transform_bounds__noop_geographic():
    crs = CRS("Pulkovo 1942")
    transformer = Transformer.from_crs(crs.geodetic_crs, crs, always_xy=True)
    assert_almost_equal(
        transformer.transform_bounds(*crs.area_of_use.bounds),
        crs.area_of_use.bounds,
    )


def test_transform_bounds__north_pole():
    crs = CRS("EPSG:32661")
    transformer = Transformer.from_crs(crs, "EPSG:4326")
    minx, miny, maxx, maxy = crs.area_of_use.bounds
    bounds = transformer.transform_bounds(miny, minx, maxy, maxx, direction="INVERSE")
    assert_almost_equal(
        bounds,
        (
            -1405880.72,
            -1371213.76,
            5405880.72,
            5371213.76,
        ),
        decimal=0,
    )
    assert_almost_equal(
        transformer.transform_bounds(*bounds),
        (48.656, -180.0, 90.0, 180.0),
        decimal=0,
    )


def test_transform_bounds__north_pole__xy():
    crs = CRS("EPSG:32661")
    transformer = Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
    bounds = transformer.transform_bounds(*crs.area_of_use.bounds, direction="INVERSE")
    assert_almost_equal(
        bounds,
        (-1371213.76, -1405880.72, 5371213.76, 5405880.72),
        decimal=0,
    )
    assert_almost_equal(
        transformer.transform_bounds(*bounds),
        (-180.0, 48.656, 180.0, 90.0),
        decimal=0,
    )


def test_transform_bounds__south_pole():
    crs = CRS("EPSG:32761")
    transformer = Transformer.from_crs(crs, "EPSG:4326")
    minx, miny, maxx, maxy = crs.area_of_use.bounds
    bounds = transformer.transform_bounds(miny, minx, maxy, maxx, direction="INVERSE")
    assert_almost_equal(
        bounds,
        (
            -1405880.72,
            -1371213.76,
            5405880.72,
            5371213.76,
        ),
        decimal=0,
    )
    assert_almost_equal(
        transformer.transform_bounds(*bounds),
        (-90, -180.0, -48.656, 180.0),
        decimal=0,
    )


def test_transform_bounds__south_pole__xy():
    crs = CRS("EPSG:32761")
    transformer = Transformer.from_crs(crs, "EPSG:4326", always_xy=True)
    bounds = transformer.transform_bounds(*crs.area_of_use.bounds, direction="INVERSE")
    assert_almost_equal(
        bounds,
        (-1371213.76, -1405880.72, 5371213.76, 5405880.72),
        decimal=0,
    )
    assert_almost_equal(
        transformer.transform_bounds(*bounds),
        (-180.0, -90.0, 180.0, -48.656),
        decimal=0,
    )


@pytest.mark.parametrize("inplace", [True, False])
def test_transform__fortran_order(inplace):
    lons, lats = numpy.arange(-180, 180, 20), numpy.arange(-90, 90, 10)
    lats, lons = numpy.meshgrid(lats, lons)
    f_lons, f_lats = lons.copy(order="F"), lats.copy(order="F")
    transformer = Transformer.from_crs(
        "EPSG:4326",
        "EPSG:6933",
        always_xy=True,
    )
    xxx, yyy = transformer.transform(lons, lats)
    f_xxx, f_yyy = transformer.transform(f_lons, f_lats, inplace=inplace)
    assert f_lons.flags.f_contiguous
    assert f_lats.flags.f_contiguous
    assert not f_xxx.flags.f_contiguous
    assert f_xxx.flags.c_contiguous
    assert not f_yyy.flags.f_contiguous
    assert f_yyy.flags.c_contiguous
    assert_array_equal(xxx, f_xxx)
    assert_array_equal(yyy, f_yyy)


def test_4d_transform__inplace__array():
    transformer = Transformer.from_crs(7789, 8401)
    xarr = array("d", [3496737.2679])
    yarr = array("d", [743254.4507])
    zarr = array("d", [5264462.9620])
    tarr = array("d", [2019.0])
    t_xarr, t_yarr, t_zarr, t_tarr = transformer.transform(
        xx=xarr, yy=yarr, zz=zarr, tt=tarr, inplace=True
    )
    assert xarr is t_xarr
    assert_almost_equal(xarr[0], 3496737.757717311)
    assert yarr is t_yarr
    assert_almost_equal(yarr[0], 743253.9940103051)
    assert zarr is t_zarr
    assert_almost_equal(zarr[0], 5264462.701132784)
    assert tarr is t_tarr
    assert_almost_equal(tarr[0], 2019.0)


def test_4d_transform__inplace__array__int():
    transformer = Transformer.from_crs(7789, 8401)
    xarr = array("i", [3496737])
    yarr = array("i", [743254])
    zarr = array("i", [5264462])
    tarr = array("i", [2019])
    t_xarr, t_yarr, t_zarr, t_tarr = transformer.transform(
        xx=xarr, yy=yarr, zz=zarr, tt=tarr, inplace=True
    )
    assert xarr is not t_xarr
    assert xarr[0] == 3496737
    assert yarr is not t_yarr
    assert yarr[0] == 743254
    assert zarr is not t_zarr
    assert zarr[0] == 5264462
    assert tarr is not t_tarr
    assert tarr[0] == 2019


def test_4d_transform__inplace__numpy():
    transformer = Transformer.from_crs(7789, 8401)
    xarr = numpy.array([3496737.2679], dtype=numpy.float64)
    yarr = numpy.array([743254.4507], dtype=numpy.float64)
    zarr = numpy.array([5264462.9620], dtype=numpy.float64)
    tarr = numpy.array([2019.0], dtype=numpy.float64)
    t_xarr, t_yarr, t_zarr, t_tarr = transformer.transform(
        xx=xarr, yy=yarr, zz=zarr, tt=tarr, inplace=True
    )
    assert xarr is t_xarr
    assert_almost_equal(xarr[0], 3496737.757717311)
    assert yarr is t_yarr
    assert_almost_equal(yarr[0], 743253.9940103051)
    assert zarr is t_zarr
    assert_almost_equal(zarr[0], 5264462.701132784)
    assert tarr is t_tarr
    assert_almost_equal(tarr[0], 2019.0)


def test_4d_transform__inplace__numpy__int():
    transformer = Transformer.from_crs(7789, 8401)
    xarr = numpy.array([3496737], dtype=numpy.int32)
    yarr = numpy.array([743254], dtype=numpy.int32)
    zarr = numpy.array([5264462], dtype=numpy.int32)
    tarr = numpy.array([2019], dtype=numpy.int32)
    t_xarr, t_yarr, t_zarr, t_tarr = transformer.transform(
        xx=xarr, yy=yarr, zz=zarr, tt=tarr, inplace=True
    )
    assert xarr is not t_xarr
    assert xarr[0] == 3496737
    assert yarr is not t_yarr
    assert yarr[0] == 743254
    assert zarr is not t_zarr
    assert zarr[0] == 5264462
    assert tarr is not t_tarr
    assert tarr[0] == 2019


def test_transformer_source_target_crs():
    transformer = Transformer.from_crs("EPSG:4326", "EPSG:4258")
    assert transformer.source_crs == "EPSG:4326"
    assert transformer.target_crs == "EPSG:4258"


def test_transformer_source_target_crs__none():
    transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000")
    assert transformer.source_crs is None
    assert transformer.target_crs is None


def test_pickle_transformer_from_pipeline():
    transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000")
    assert transformer == pickle.loads(pickle.dumps(transformer))


def test_pickle_transformer_from_crs():
    transformer = Transformer.from_crs(
        "EPSG:4326",
        "EPSG:2964",
        always_xy=True,
        area_of_interest=AreaOfInterest(-136.46, 49.0, -60.72, 83.17),
    )
    assert transformer == pickle.loads(pickle.dumps(transformer))


def test_unpickle_transformer_from_crs_v1_3():
    pickled_transformer = (
        b"\x80\x04\x95p\x01\x00\x00\x00\x00\x00\x00\x8c\x12"
        b"pyproj.transformer\x94\x8c\x0bTransformer\x94\x93\x94)"
        b"\x81\x94}\x94\x8c\x12_transformer_maker\x94h\x00\x8c\x12"
        b"TransformerFromCRS\x94\x93\x94)\x81\x94}\x94(\x8c\x08"
        b"crs_from\x94C\tEPSG:4326\x94\x8c\x06crs_to\x94C\tEPSG:2964"
        b"\x94\x8c\talways_xy\x94\x88\x8c\x10area_of_interest\x94\x8c\n"
        b"pyproj.aoi\x94\x8c\x0eAreaOfInterest\x94\x93\x94)\x81\x94}\x94"
        b"(\x8c\x0fwest_lon_degree\x94G\xc0a\x0e\xb8Q\xeb\x85\x1f\x8c\x10"
        b"south_lat_degree\x94G@H\x80\x00\x00\x00\x00\x00\x8c\x0f"
        b"east_lon_degree\x94G\xc0N\\(\xf5\xc2\x8f\\\x8c\x10"
        b"north_lat_degree\x94G@T\xca\xe1G\xae\x14"
        b"{ub\x8c\tauthority\x94N\x8c\x08accuracy\x94N\x8c\x0eallow_ballpark\x94Nubsb."
    )
    transformer = Transformer.from_crs(
        "EPSG:4326",
        "EPSG:2964",
        always_xy=True,
        area_of_interest=AreaOfInterest(-136.46, 49.0, -60.72, 83.17),
    )
    assert transformer == pickle.loads(pickled_transformer)


def test_transformer_group_accuracy_filter():
    group = TransformerGroup("EPSG:4326", "EPSG:4258", accuracy=0.05)
    assert not group.transformers
    assert not group.unavailable_operations


def test_transformer_group_allow_ballpark_filter():
    group = TransformerGroup(
        "EPSG:4326", "EPSG:4258", authority="PROJ", allow_ballpark=False
    )
    if PROJ_GTE_990:
        assert len(group.transformers) == 1
        assert all(
            "Ballpark" not in transformer.description
            for transformer in group.transformers
        )
    else:
        assert not group.transformers
    assert not group.unavailable_operations


def test_transformer_group_allow_superseded_filter():
    default_group = TransformerGroup(4203, 4326)
    superseded_group = TransformerGroup(4203, 4326, allow_superseded=True)
    assert len(superseded_group.transformers) > len(default_group.transformers)


def test_transformer_group_allow_superseded_always_xy():
    """Test that TransformerGroup works with both allow_superseded and always_xy."""
    # This combination used to fail with "Input is not a transformation" error
    # because proj_normalize_for_visualization() returns NULL for certain
    # concatenated operations (e.g., EPSG:8047).
    group = TransformerGroup(4230, 4326, allow_superseded=True, always_xy=True)
    assert len(group.transformers) >= 1
    # Operations that cannot be normalized for visualization should be in
    # unavailable_operations, not silently dropped
    total_ops = len(group.transformers) + len(group.unavailable_operations)
    assert total_ops >= 1


def test_transformer_group_crs_extent_use_none():
    group_default = TransformerGroup(4230, 32632)
    group_no_extent = TransformerGroup(4230, 32632, crs_extent_use=CRSExtentUse.NONE)
    # lengths may differ depending on PROJ heuristic, but group_no_extent should
    # be longer than group_default
    assert len(group_default.transformers) >= 1
    assert len(group_no_extent.transformers) >= 1
    assert len(group_no_extent.transformers) >= len(group_default.transformers)


def test_transformer_group_crs_extent_use_invalid():
    # invalid crs_extent_use should raise a ValueError via BaseEnum.create
    with pytest.raises(ValueError):
        TransformerGroup(4326, 3857, crs_extent_use="invalid-option")


def test_transformer_group_crs_extent_use_intersection_enum():
    group_intersection = TransformerGroup(
        4230, 32632, crs_extent_use=CRSExtentUse.INTERSECTION
    )
    assert len(group_intersection.transformers) >= 1


def test_transformer_group_crs_extent_use_smallest_enum():
    group_smallest = TransformerGroup(4230, 32632, crs_extent_use=CRSExtentUse.SMALLEST)
    assert len(group_smallest.transformers) >= 1


def test_transformer_group_crs_extent_use_both_enum():
    group_both = TransformerGroup(4230, 32632, crs_extent_use=CRSExtentUse.BOTH)
    assert len(group_both.transformers) >= 1


def test_transformer_group_pivot_crs_mode_never():
    group = TransformerGroup(4326, 3857, pivot_crs=IntermediateCRSUse.NEVER)
    assert len(group.transformers) >= 1


def test_transformer_group_pivot_crs_string_list():
    group = TransformerGroup(4230, 4326, pivot_crs="EPSG:4326,EPSG:4979")
    assert len(group.transformers) >= 1


def test_transformer_group_pivot_crs_iterable_codes():
    group = TransformerGroup(4326, 4979, pivot_crs=["EPSG:4978", 4326])
    assert len(group.transformers) >= 1


def test_transformer_group_pivot_crs_invalid_empty_string():
    with pytest.raises(ProjError):
        TransformerGroup(4326, 3857, pivot_crs="  ")


def test_transformer_group_pivot_crs_unidentifiable_crs():
    custom_crs = CRS.from_proj4("+proj=longlat +a=1 +b=1")
    with pytest.raises(ProjError):
        TransformerGroup(4326, 3857, pivot_crs=custom_crs)


def test_transformer_group_pivot_crs_single_crs_object():
    """Test pivot_crs with a single CRS object (not in a list)."""
    crs = CRS.from_epsg(4326)
    group = TransformerGroup(4230, 4326, pivot_crs=crs)
    assert len(group.transformers) >= 1


def test_transformer_group_pivot_crs_empty_iterable():
    """Test pivot_crs with an empty iterable raises error."""
    with pytest.raises(ProjError, match="at least one CRS"):
        TransformerGroup(4326, 3857, pivot_crs=[])


def test_transformer_group_pivot_crs_string_only_commas():
    """Test pivot_crs string with only commas/whitespace raises error."""
    with pytest.raises(ProjError):
        TransformerGroup(4326, 3857, pivot_crs=",  , ,")


def test_transformer_group_pivot_crs_integer_code():
    """Test pivot_crs with an integer EPSG code."""
    group = TransformerGroup(4230, 4326, pivot_crs=4326)
    assert len(group.transformers) >= 1


def test_transformer_group_authority_filter():
    group = TransformerGroup("EPSG:4326", "EPSG:4258", authority="PROJ")
    if PROJ_GTE_990:
        assert len(group.transformers) == 2
    else:
        assert len(group.transformers) == 1
    assert not group.unavailable_operations
    assert any(
        transformer.description == "Ballpark geographic offset from WGS 84 to ETRS89"
        for transformer in group.transformers
    )


def test_transformer_force_over():
    transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", force_over=True)
    # Test a point along the equator.
    # The same point, but in two different representations.
    xxx, yyy = transformer.transform(0, 140)
    xxx_over, yyy_over = transformer.transform(0, -220)
    # Web Mercator x's between 0 and 180 longitude come out positive.
    # But when forcing the over flag, the -220 calculation makes it flip.
    assert xxx > 0
    assert xxx_over < 0
    # check it works in both directions
    xxx_inverse, yyy_inverse = transformer.transform(
        xxx, yyy, direction=TransformDirection.INVERSE
    )
    xxx_over_inverse, yyy_over_inverse = transformer.transform(
        xxx_over, yyy_over, direction=TransformDirection.INVERSE
    )
    assert_almost_equal(xxx_inverse, 0)
    assert_almost_equal(xxx_over_inverse, 0)
    assert_almost_equal(yyy_inverse, 140)
    assert_almost_equal(yyy_over_inverse, -220)


def test_transformer__get_last_used_operation():
    transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857")
    with pytest.raises(
        ProjError,
        match=(
            r"Last used operation not found\. "
            r"This is likely due to not initiating a transform\."
        ),
    ):
        transformer.get_last_used_operation()
    xxx, yyy = transformer.transform(1, 2)
    operation = transformer.get_last_used_operation()
    assert isinstance(operation, Transformer)
    assert xxx, yyy == operation.transform(1, 2)


def test_transformer_group_grid_check_none():
    """Test grid_check='none' includes operations even if grids are missing.

    With grid_check='none', all operations are returned regardless of
    grid availability. Operations requiring missing grids appear in
    unavailable_operations.
    """
    tg = TransformerGroup(
        "EPSG:4230",
        "EPSG:4326",
        crs_extent_use="none",
        authority="any",
        grid_check="none",
    )

    # Should have transformers available
    assert len(tg.transformers) >= 1

    # Total operations should be at least 1
    total_ops = len(tg.transformers) + len(tg.unavailable_operations)
    assert total_ops >= 1


def test_transformer_group_grid_check_discard_missing():
    """Test grid_check='discard_missing' excludes operations with missing grids.

    With grid_check='discard_missing', operations requiring missing grids
    are completely excluded from results.
    """
    tg_discard = TransformerGroup(
        "EPSG:4230",
        "EPSG:4326",
        crs_extent_use="none",
        authority="any",
        grid_check="discard_missing",
    )

    # With discard_missing, there should be no unavailable operations
    assert len(tg_discard.unavailable_operations) == 0

    # Should still have some transformers available
    assert len(tg_discard.transformers) >= 1

    # Compare with grid_check='none' - discard_missing should have
    # equal or fewer total operations (some may be discarded)
    if not grids_available():
        tg_none = TransformerGroup(
            "EPSG:4230",
            "EPSG:4326",
            crs_extent_use="none",
            authority="any",
            grid_check="none",
        )
        # With discard_missing, we should have no more transformers than
        # the total (transformers + unavailable) from grid_check='none'
        total_none = len(tg_none.transformers) + len(tg_none.unavailable_operations)
        assert len(tg_discard.transformers) <= total_none


def test_transformer_group_pivot_crs_filters_intermediate():
    """
    Test that specifying a pivot CRS filters out operations
    using other intermediates.

    OSGB36 (UK) to NTF (France): EPSG:4277 → EPSG:4275
    With pivot_crs='always', one operation uses ED50 as intermediate.
    With pivot_crs='EPSG:4326', that ED50 operation should be filtered out.
    """
    # With pivot_crs='always' - allows any intermediate CRS (including ED50)
    tg_always = TransformerGroup(
        "EPSG:4277",
        "EPSG:4275",
        crs_extent_use="none",
        authority="any",
        pivot_crs="always",
        grid_check="discard_missing",
    )

    # With pivot_crs='EPSG:4326' - restricts to WGS84 as intermediate
    tg_wgs84 = TransformerGroup(
        "EPSG:4277",
        "EPSG:4275",
        crs_extent_use="none",
        authority="any",
        pivot_crs="EPSG:4326",
        grid_check="discard_missing",
    )

    # Both should have valid transformers
    assert len(tg_always.transformers) >= 1
    assert len(tg_wgs84.transformers) >= 1

    # The ED50 operation filtering test only applies when grids are available
    # (either via network or locally installed)
    if grids_available():
        # The 'always' mode should include the ED50 operation
        always_descs = {t.description for t in tg_always.transformers}
        ed50_operation = "OSGB36 to ED50 (1) + Inverse of NTF to ED50 (1)"
        assert ed50_operation in always_descs, (
            f"Expected ED50 operation in 'always' mode, got: {always_descs}"
        )

        # The EPSG:4326 mode should NOT include the ED50 operation
        wgs84_descs = {t.description for t in tg_wgs84.transformers}
        assert ed50_operation not in wgs84_descs, (
            "ED50 operation should be filtered out with pivot_crs='EPSG:4326'"
        )

        # WGS84 mode should have one fewer operation than 'always'
        assert len(tg_wgs84.transformers) < len(tg_always.transformers), (
            f"Expected fewer operations with specific pivot CRS: "
            f"{len(tg_wgs84.transformers)} vs {len(tg_always.transformers)}"
        )


def test_transformer_group_grid_check_with_pivot_discard_missing():
    """Test combined pivot_crs and grid_check='discard_missing'.

    NTF (France) to MGI (Austria): EPSG:4275 → EPSG:4312
    When using both pivot_crs and grid_check='discard_missing', operations
    should be filtered by both pivot CRS and grid availability.
    """
    tg = TransformerGroup(
        "EPSG:4275",
        "EPSG:4312",
        pivot_crs="EPSG:4326",
        crs_extent_use="none",
        authority="any",
        grid_check="discard_missing",
    )

    # Should have some transformers available
    assert len(tg.transformers) >= 1

    # With discard_missing, there should be no unavailable operations
    assert len(tg.unavailable_operations) == 0
