#!/usr/bin/env pytest
# -*- coding: utf-8 -*-
###############################################################################
# $Id$
#
# Project:  GDAL/OGR Test Suite
# Purpose:  Test read functionality for OGR PGEO driver.
# Author:   Even Rouault <even dot rouault at spatialys.com>
#
###############################################################################
# Copyright (c) 2010-2012, Even Rouault <even dot rouault at spatialys.com>
#
# SPDX-License-Identifier: MIT
###############################################################################

import os
import shutil
import sys

import gdaltest
import ogrtest
import pytest

from osgeo import gdal, ogr


###############################################################################
@pytest.fixture(autouse=True, scope="module")
def module_disable_exceptions():
    with gdaltest.disable_exceptions():
        yield


@pytest.fixture(scope="module", autouse=True)
def setup_driver():
    driver = ogr.GetDriverByName("PGeo")
    if driver is None:
        pytest.skip("PGeo driver not available", allow_module_level=True)

    # remove mdb driver
    mdb_driver = ogr.GetDriverByName("MDB")
    if mdb_driver is not None:
        mdb_driver.Deregister()

    # we may have the PGeo GDAL driver, but be missing an ODBC driver for MS Access on the test environment
    # so open a test dataset and check to see if it's supported
    with gdaltest.disable_exceptions():
        pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    if "MDB_ODBC_DRIVER_INSTALLED" in os.environ:
        # if environment variable is set, then we know that the ODBC driver is installed and something
        # unexpected has happened (i.e. GDAL driver is broken!)
        assert pgeo_ds is not None
    elif pgeo_ds is None:
        pytest.skip(
            "could not open DB. MDB ODBC driver probably missing or misconfigured",
            allow_module_level=True,
        )

    yield

    if mdb_driver is not None:
        print("Reregistering MDB driver")
        mdb_driver.Register()


@pytest.fixture()
def download_test_data():
    gdaltest.download_or_skip(
        "http://download.osgeo.org/gdal/data/pgeo/PGeoTest.zip", "PGeoTest.zip"
    )

    try:
        os.stat("tmp/cache/Autodesk Test.mdb")
    except OSError:
        try:
            gdaltest.unzip("tmp/cache", "tmp/cache/PGeoTest.zip")
            try:
                os.stat("tmp/cache/Autodesk Test.mdb")
            except OSError:
                pytest.skip()
        except Exception:
            pytest.skip()

    pgeo_ds = ogr.Open("tmp/cache/Autodesk Test.mdb")
    if pgeo_ds is None:
        pytest.skip("could not open DB. Driver probably misconfigured")

    return pgeo_ds


@pytest.fixture()
def ogrsf_path():
    import test_cli_utilities

    path = test_cli_utilities.get_test_ogrsf_path()
    if path is None:
        pytest.skip("ogrsf test utility not found")

    return path


def recent_enough_mdb_odbc_driver():
    # At time of writing, mdbtools <= 0.9.4 has some deficiencies
    # See https://github.com/OSGeo/gdal/pull/4354#issuecomment-907455798 for details
    # So allow some tests only or Windows, or on a local machine that don't have the CI environment variable set
    return sys.platform == "win32" or "CI" not in os.environ


###############################################################################
# Basic testing - PGeo v10 format


def test_ogr_pgeo_basic():
    pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    assert pgeo_ds is not None

    assert pgeo_ds.GetLayerCount() == 4, "did not get expected layer count"

    lyr = pgeo_ds.GetLayer(0)
    assert lyr is not None
    assert lyr.GetName() == "lines", "did not get expected layer name"
    assert (
        lyr.GetGeomType() == ogr.wkbMultiLineString
    ), "did not get expected layer geometry type"
    assert lyr.GetFeatureCount() == 6, "did not get expected feature count"

    feat = lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 1
        or feat.GetField("Name") != "Highway"
        or feat.GetField("Value_") != 1
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    ogrtest.check_feature_geometry(
        feat,
        "MULTILINESTRING ((-117.623198392192 35.1986535259395,-117.581273749384 35.1986535259395,-117.078178041084 35.3244274525649,-116.868554829742 35.5340506648064,-116.617006974692 35.8694478036733,-116.491233048067 35.9532970883899,-116.1558359092 36.2886942272568,-116.071986624483 36.3725435119736,-115.443116988658 36.7498652936484,-114.814247352833 37.0433377906066,-114.311151644533 37.169111717232,-113.38880951175 37.3787349294735,-113.095337014791 37.3787349294735,-112.592241306491 37.3787349294735,-111.753748459324 37.2948856447567,-111.502200604274 37.2529610028481,-111.082954180691 37.1271870753233,-110.747557041824 37.1271870753233,-110.160612048807 36.9175638630819,-110.034838121282 36.8337145783652,-109.741365624324 36.7498652936484,-109.573667054891 36.6660160089317,-109.238269916024 36.4983174394983,-109.07057134659 36.4144681547816,-108.81902349244 36.2886942272568,-108.693249564915 36.2467695853481,-108.483626353573 36.1629203006314,-107.645133505507 35.9113724464813,-106.597017446098 35.8694478036733,-106.05199709499 35.7017492342398,-105.80044924084 35.6178999495231,-105.590826028598 35.5759753076144,-105.297353532539 35.5759753076144,-104.961956393673 35.5759753076144,-104.710408538623 35.5340506648064,-104.458860684473 35.4921260219984,-103.871915691456 35.4921260219984,-103.788066406739 35.4921260219984,-103.326895340348 35.4082767372817,-102.949573558673 35.4082767372817,-102.488402493181 35.4502013800897,-102.069156068698 35.4502013800897,-101.482211075681 35.4502013800897,-100.937190724572 35.6598245923311,-100.308321088747 35.8694478036733,-100.056773234597 36.0371463731067,-99.0505818179965 36.0790710159147,-97.6670686188216 35.7436738770479,-97.1639729105214 35.6178999495231,-96.1158568511128 35.5340506648064,-95.6127611419132 35.5340506648064,-94.3969465130711 35.9113724464813,-93.6842275925293 36.2886942272568,-92.9295840300789 36.8337145783652,-92.2587897523451 37.169111717232,-91.6299201165201 37.5045088569982,-90.4141054867787 37.8818306377738,-90.4141054867787 37.8818306377738,-90.2464069173453 37.9237552805818,-89.4917633548949 37.8399059958651,-89.156366216028 37.6722074264316,-88.485571937395 37.5045088569982,-87.8147776596612 37.2529610028481,-87.5632298055111 37.169111717232,-87.1439833810282 37.0433377906066,-85.9700933949941 36.8756392211732,-85.8023948255607 36.8756392211732,-84.0834844875191 36.9594885058899,-84.0415598456103 37.0433377906066,-82.9515191433937 37.5464334989069,-82.6999712892436 37.6302827836236))",
        max_error=0.0000001,
    )

    lyr = pgeo_ds.GetLayer(1)
    assert lyr is not None
    assert lyr.GetName() == "points", "did not get expected layer name"
    assert lyr.GetGeomType() == ogr.wkbPoint, "did not get expected layer geometry type"
    assert lyr.GetFeatureCount() == 17, "did not get expected feature count"

    feat = lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 1
        or feat.GetField("Class") != "Jet"
        or feat.GetField("Heading") != 90
        or feat.GetField("Importance") != 3
        or feat.GetField("Pilots") != 2
        or feat.GetField("Cabin_Crew") != 0
        or feat.GetField("Staff") != 2
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    ogrtest.check_feature_geometry(
        feat, "POINT (-117.232574188648 37.2187715665546)", max_error=0.0000001
    )

    lyr = pgeo_ds.GetLayer(2)
    assert lyr is not None
    assert lyr.GetName() == "polys", "did not get expected layer name"
    assert (
        lyr.GetGeomType() == ogr.wkbMultiPolygon
    ), "did not get expected layer geometry type"
    assert lyr.GetFeatureCount() == 10, "did not get expected feature count"

    feat = lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 1
        or feat.GetField("Name") != "Lake"
        or feat.GetField("Value_") != 10
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    ogrtest.check_feature_geometry(
        feat,
        "MULTIPOLYGON (((-109.189702908462 45.4535155983662,-108.967868942498 45.7308080551477,-108.635117994 46.0081005128284,-108.135991571254 46.2299344778939,-107.692323640224 46.2853929696099,-107.415031183442 46.3408514613259,-106.915904760696 46.3408514613259,-106.472236828767 46.3408514613259,-106.139485880269 46.1744759870772,-105.97311040602 46.0635590036451,-105.862193423488 45.8971835293964,-105.640359457523 45.7308080551477,-105.52944247499 45.3425986158335,-105.473983983274 45.2871401241175,-105.418525492457 45.009847667336,-105.252150018209 44.7880137013713,-104.863940577995 44.5107212445898,-104.697565103747 44.3998042611578,-104.586648121214 44.233428786909,-104.531189629498 43.9561363301276,-104.531189629498 43.6788438733461,-104.586648121214 43.3460929248487,-104.697565103747 43.1797174505999,-104.863940577995 42.9578834846351,-105.030316052244 42.7915080103865,-105.363067000741 42.5696740444217,-105.473983983274 42.514215553605,-106.361319846234 42.2923815876402,-106.583153812199 42.4032985701729,-107.026821743229 42.7360495186704,-107.137738725762 42.8469665021024,-107.193197217478 43.0688004671679,-107.193197217478 43.2351759414166,-107.193197217478 43.4570099073813,-107.248655709194 43.8452193475949,-107.248655709194 43.8452193475949,-107.359572691726 43.9006778384116,-107.581406657691 43.9006778384116,-107.858699114473 43.9006778384116,-107.969616097005 43.9006778384116,-108.080533080437 43.7897608558789,-108.302367045503 43.7897608558789,-108.635117994 43.9006778384116,-108.967868942498 44.233428786909,-109.189702908462 44.3443457703411,-109.300619890995 44.5107212445898,-109.300619890995 44.7880137013713,-109.300619890995 45.0653061581527,-109.300619890995 45.1762231415848,-109.189702908462 45.4535155983662),(-107.750165309282 45.3597163907988,-107.951114759139 45.0381972704877,-107.870734979736 44.8774377107818,-107.830545089584 44.7568680403278,-107.70997541913 44.6362983707733,-107.589405749576 44.6362983707733,-107.14731695971 44.5157287012186,-107.066937179407 44.435348920916,-106.946367509853 44.3951590307647,-106.86598772955 44.2343994710588,-106.745418059096 44.0736399104536,-106.58465849939 44.0334500212016,-106.464088829836 44.0334500212016,-106.263139379978 44.0334500212016,-106.222949489827 44.0736399104536,-106.102379819373 44.1540196907562,-106.062189929222 44.3549691406134,-106.102379819373 44.5157287012186,-106.142569709524 44.7166781510759,-106.263139379978 44.8372478206305,-106.343519159382 44.9176276009331,-106.464088829836 44.9980073803364,-106.58465849939 45.0381972704877,-106.745418059096 45.1989568310929,-106.785607949247 45.2391467203449,-106.906177619701 45.3597163907988,-106.986557399105 45.5204759505048,-107.227696739113 45.5606658406561,-107.468836079122 45.5204759505048,-107.750165309282 45.3597163907988)),((-103.490036969431 45.9625647403705,-103.409657189128 46.2037040803791,-103.289087519573 46.4448434203876,-103.007758289414 46.5654130908415,-102.525479609396 46.7261726505474,-102.284340269388 46.6859827603961,-102.163770599833 46.5654130908415,-102.083390819531 46.4448434203876,-102.003011039228 46.324273750833,-101.842251479522 46.1635141911271,-101.721681809967 46.0429445206731,-101.681491919816 45.9223748511185,-101.681491919816 45.7616152905133,-101.681491919816 45.6410456209587,-101.802061589371 45.4802860603535,-101.882441369673 45.3597163907988,-101.962821149976 45.3195265006476,-102.083390819531 45.1587669409416,-102.123580709682 45.1185770507903,-102.324530159539 45.1185770507903,-102.445099829993 45.1185770507903,-102.565669499548 45.1185770507903,-102.967568399262 45.1989568310929,-102.967568399262 45.2391467203449,-103.168517849119 45.3999062809502,-103.208707739271 45.4802860603535,-103.369467299876 45.5606658406561,-103.490036969431 45.9625647403705)))",
        max_error=0.0000001,
    )


###############################################################################
# Basic testing - PGeo v9 format


def test_ogr_pgeo_basic_v9(download_test_data):
    assert download_test_data.GetLayerCount() == 3, "did not get expected layer count"

    lyr = download_test_data.GetLayer(0)
    feat = lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 1
        or feat.GetField("IDNUM") != 9424
        or feat.GetField("OWNER") != "City"
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    ogrtest.check_feature_geometry(
        feat,
        "MULTILINESTRING ((1910941.703951031 445833.57942859828 0,1910947.927691862 445786.43811868131 0))",
        max_error=0.0000001,
    )

    feat_count = lyr.GetFeatureCount()
    assert feat_count == 9418, "did not get expected feature count"


###############################################################################
# Test LIST_ALL_TABLES open option


def test_ogr_pgeo_list_all_tables():
    pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    assert pgeo_ds is not None

    assert pgeo_ds.GetLayerCount() == 4, "did not get expected layer count"

    # Test LIST_ALL_TABLES=YES open option
    pgeo_ds_all_table = gdal.OpenEx(
        "data/pgeo/sample.mdb", gdal.OF_VECTOR, open_options=["LIST_ALL_TABLES=YES"]
    )

    # Depending on the actual ODBC driver used (i.e Microsoft driver vs mdbtools driver), a different
    # set of system tables are exposed (18 vs 27). Mdbtools includes the various MSys* tables, while the
    # Microsoft driver strips these out. Here we test only for the common subset of tables.
    assert pgeo_ds_all_table.GetLayerCount() >= 18, "did not get expected layer count"
    layer_names = [
        pgeo_ds_all_table.GetLayer(i).GetName()
        for i in range(pgeo_ds_all_table.GetLayerCount())
    ]

    for name in [
        "lines",
        "points",
        "polys",
        "GDB_ColumnInfo",
        "GDB_DatabaseLocks",
        "GDB_GeomColumns",
        "GDB_ItemRelationships",
        "GDB_ItemRelationshipTypes",
        "GDB_Items",
        "GDB_Items_Shape_Index",
        "GDB_ItemTypes",
        "GDB_RasterColumns",
        "GDB_ReplicaLog",
        "GDB_SpatialRefs",
        "lines_Shape_Index",
        "non_spatial",
        "points_Shape_Index",
        "polys_Shape_Index",
    ]:
        assert name in layer_names

    private_layers = [
        pgeo_ds_all_table.GetLayer(i).GetName()
        for i in range(pgeo_ds_all_table.GetLayerCount())
        if pgeo_ds_all_table.IsLayerPrivate(i)
    ]
    for name in [
        "GDB_ColumnInfo",
        "GDB_DatabaseLocks",
        "GDB_GeomColumns",
        "GDB_ItemRelationships",
        "GDB_ItemRelationshipTypes",
        "GDB_Items",
        "GDB_Items_Shape_Index",
        "GDB_ItemTypes",
        "GDB_RasterColumns",
        "GDB_ReplicaLog",
        "GDB_SpatialRefs",
        "lines_Shape_Index",
        "points_Shape_Index",
        "polys_Shape_Index",
    ]:
        assert name in private_layers
    for name in ["lines", "points", "polys", "non_spatial"]:
        assert name not in private_layers


###############################################################################
# Test opening a private table by name


def test_ogr_pgeo_open_private_table():
    pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    assert pgeo_ds is not None

    assert pgeo_ds.GetLayerCount() == 4, "did not get expected layer count"

    # open a standard layer by name
    lines_lyr = pgeo_ds.GetLayerByName("lines")
    assert lines_lyr is not None
    assert (
        lines_lyr.GetGeomType() == ogr.wkbMultiLineString
    ), "did not get expected layer geometry type"
    assert lines_lyr.GetFeatureCount() == 6, "did not get expected feature count"

    gdb_items_lyr = pgeo_ds.GetLayerByName("GDB_Items")
    assert gdb_items_lyr is not None
    assert (
        gdb_items_lyr.GetGeomType() == ogr.wkbNone
    ), "did not get expected layer geometry type"
    assert gdb_items_lyr.GetFeatureCount() == 6, "did not get expected feature count"

    feat = gdb_items_lyr.GetNextFeature()
    # depending on ODBC driver, UID fields have different text representations
    assert "ECC818B5-6C5D-4447-BEF5" in feat.GetField("UUID")

    # try a second time, same layer should be returned
    gdb_items_lyr2 = pgeo_ds.GetLayerByName("GDB_Items")
    assert gdb_items_lyr2 is not None

    # a layer which doesn't exist
    other_lyr = pgeo_ds.GetLayerByName("xxx")
    assert other_lyr is None


###############################################################################
# Test spatial filter


def test_ogr_pgeo_spatial_filter():
    pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    assert pgeo_ds is not None

    lyr = pgeo_ds.GetLayer(1)

    lyr.SetSpatialFilterRect(-118.328, 34.501, -107.457, 40.820)

    feat_count = lyr.GetFeatureCount()
    assert feat_count == 4, "did not get expected feature count"

    feat = lyr.GetNextFeature()
    if feat.GetField("OBJECTID") != 1:
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    feat = lyr.GetNextFeature()
    if feat.GetField("OBJECTID") != 3:
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    feat = lyr.GetNextFeature()
    if feat.GetField("OBJECTID") != 9:
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    feat = lyr.GetNextFeature()
    if feat.GetField("OBJECTID") != 15:
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    feat = lyr.GetNextFeature()
    assert feat is None

    # Check that geometry filter is well cleared
    lyr.SetSpatialFilter(None)
    feat_count = lyr.GetFeatureCount()
    assert feat_count == 17, "did not get expected feature count"


###############################################################################
# Test spatial filter, v9 format


def test_ogr_pgeo_spatial_filter_v9(download_test_data):
    lyr = download_test_data.GetLayer(0)
    lyr.ResetReading()
    lyr.SetSpatialFilterRect(1909982.7, 445835.6, 1911646.3, 446627.5)

    feat_count = lyr.GetFeatureCount()
    assert feat_count == 18, "did not get expected feature count"

    feat = lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 6845
        or feat.GetField("IDNUM") != 2574
        or feat.GetField("OWNER") != "City"
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    # Check that geometry filter is well cleared
    lyr.SetSpatialFilter(None)
    feat_count = lyr.GetFeatureCount()
    assert feat_count == 9418, "did not get expected feature count"


###############################################################################
# Test attribute filter


def test_ogr_pgeo_attribute_filter():
    pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    assert pgeo_ds is not None

    lyr = pgeo_ds.GetLayer(1)

    lyr.SetAttributeFilter("Class='Biplane'")

    feat_count = lyr.GetFeatureCount()
    assert feat_count == 5, "did not get expected feature count"

    feat = lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 2
        or feat.GetField("Class") != "Biplane"
        or feat.GetField("Heading") != 0
        or feat.GetField("Importance") != 1
        or feat.GetField("Pilots") != 3
        or feat.GetField("Cabin_Crew") != 3
        or feat.GetField("Staff") != 6
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    # Check that attribute filter is well cleared (#3706)
    lyr.SetAttributeFilter(None)
    feat_count = lyr.GetFeatureCount()
    assert feat_count == 17, "did not get expected feature count"


###############################################################################
# Test attribute filter, v9 format data


def test_ogr_pgeo_attribute_filter_v9(download_test_data):
    lyr = download_test_data.GetLayer(0)
    lyr.SetAttributeFilter("OBJECTID=1")

    feat_count = lyr.GetFeatureCount()
    assert feat_count == 1, "did not get expected feature count"

    feat = lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 1
        or feat.GetField("IDNUM") != 9424
        or feat.GetField("OWNER") != "City"
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    # Check that attribute filter is well cleared (#3706)
    lyr.SetAttributeFilter(None)
    feat_count = lyr.GetFeatureCount()
    assert feat_count == 9418, "did not get expected feature count"


###############################################################################
# Test ExecuteSQL()


def test_ogr_pgeo_execute_sql():
    pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    assert pgeo_ds is not None

    sql_lyr = pgeo_ds.ExecuteSQL("SELECT * FROM points WHERE Class = 'B52'")

    feat_count = sql_lyr.GetFeatureCount()
    assert feat_count == 4, "did not get expected feature count"

    feat = sql_lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 10
        or feat.GetField("Class") != "B52"
        or feat.GetField("Heading") != 0
        or feat.GetField("Importance") != 10
        or feat.GetField("Pilots") != 2
        or feat.GetField("Cabin_Crew") != 1
        or feat.GetField("Staff") != 3
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    pgeo_ds.ReleaseResultSet(sql_lyr)


###############################################################################
# Test ExecuteSQL(), v9 format data


def test_ogr_pgeo_execute_sql_v9(download_test_data):
    sql_lyr = download_test_data.ExecuteSQL("SELECT * FROM SDPipes WHERE OBJECTID = 1")

    feat_count = sql_lyr.GetFeatureCount()
    assert feat_count == 1, "did not get expected feature count"

    feat = sql_lyr.GetNextFeature()
    if (
        feat.GetField("OBJECTID") != 1
        or feat.GetField("IDNUM") != 9424
        or feat.GetField("OWNER") != "City"
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    download_test_data.ReleaseResultSet(sql_lyr)


###############################################################################
# Test GetFeature()


def test_ogr_pgeo_get_feature():
    pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    assert pgeo_ds is not None

    lyr = pgeo_ds.GetLayer(1)
    feat = lyr.GetFeature(14)
    if feat.GetField("OBJECTID") != 14:
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")


###############################################################################
# Test GetFeature(), v9 format data


def test_ogr_pgeo_get_feature_v9(download_test_data):
    lyr = download_test_data.GetLayer(0)
    feat = lyr.GetFeature(9418)
    if feat.GetField("OBJECTID") != 9418:
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")


###############################################################################
# Run test_ogrsf


def test_ogr_pgeo_ogrsf(ogrsf_path):
    ret = gdaltest.runexternal(ogrsf_path + " data/pgeo/sample.mdb")

    assert ret.find("INFO") != -1 and ret.find("ERROR") == -1


###############################################################################
# Run test_ogrsf, v9 format data


def test_ogr_pgeo_ogrsf_v9(download_test_data, ogrsf_path):
    ret = gdaltest.runexternal(ogrsf_path + ' "tmp/cache/Autodesk Test.mdb"')

    assert ret.find("INFO") != -1 and ret.find("ERROR") == -1


###############################################################################
# Run test_ogrsf with -sql


def test_ogr_pgeo_ogrsf_sql(ogrsf_path):
    ret = gdaltest.runexternal(
        ogrsf_path + ' "data/pgeo/sample.mdb" -sql "SELECT * FROM points"'
    )

    assert ret.find("INFO") != -1 and ret.find("ERROR") == -1


###############################################################################
# Run test_ogrsf with -sql, v9 format data


def test_ogr_pgeo_ogrsf_sql_v9(download_test_data, ogrsf_path):
    ret = gdaltest.runexternal(
        ogrsf_path + ' "tmp/cache/Autodesk Test.mdb" -sql "SELECT * FROM SDPipes"'
    )

    assert ret.find("INFO") != -1 and ret.find("ERROR") == -1


###############################################################################
# Open mdb with non-spatial tables


def test_ogr_pgeo_non_spatial():
    pgeo_ds = ogr.Open("data/pgeo/sample.mdb")
    assert pgeo_ds.GetLayerCount() == 4, "did not get expected layer count"

    layer_names = [pgeo_ds.GetLayer(n).GetName() for n in range(4)]
    assert set(layer_names) == {
        "lines",
        "polys",
        "points",
        "non_spatial",
    }, "did not get expected layer names"

    non_spatial_layer = pgeo_ds.GetLayerByName("non_spatial")
    feat = non_spatial_layer.GetNextFeature()
    if (
        feat.GetField("text_field") != "Record 1"
        or feat.GetField("int_field") != 13
        or feat.GetField("long_int_field") != 10001
        or feat.GetField("float_field") != 13.5
        or feat.GetField("double_field") != 14.5
    ):
        feat.DumpReadable()
        pytest.fail("did not get expected attributes")

    if recent_enough_mdb_odbc_driver():
        assert feat.GetField("date_field") == "2020/01/30 00:00:00"

    feat_count = non_spatial_layer.GetFeatureCount()
    assert feat_count == 2, "did not get expected feature count"


##################################################################################
# Open mdb with polygon layer containing a mix of single and multi-part geometries


def test_ogr_pgeo_mixed_single_multi_polygons():
    pgeo_ds = ogr.Open("data/pgeo/mixed_types.mdb")

    polygon_layer = pgeo_ds.GetLayerByName("polygons")
    assert polygon_layer.GetGeomType() == ogr.wkbMultiPolygon

    # The PGeo format has a similar approach to multi-part handling as Shapefiles,
    # where polygon and multipolygon geometries or line and multiline geometries will
    # co-exist in a layer reported as just polygon or line type respectively.
    # To handle this in a predictable way for clients we always promote the polygon/line
    # types to multitypes, and correspondingly ALWAYS return multi polygon/line geometry
    # objects for features (even if strictly speaking the original feature had a polygon/line
    # geometry object)
    feat = polygon_layer.GetNextFeature()
    ogrtest.check_feature_geometry(
        feat,
        "MULTIPOLYGON (((-11315979.9947 6171775.831,-10597634.808 6140025.7675,-11331855.0265 5477243.192,-11315979.9947 6171775.831)))",
        max_error=0.0000001,
    )

    feat = polygon_layer.GetNextFeature()
    ogrtest.check_feature_geometry(
        feat,
        "MULTIPOLYGON (((-9855477.0737 5596305.9301,-9581632.776 5258961.5054,-9863414.5896 5258961.5054,-9855477.0737 5596305.9301)),((-10101540.0658 6092400.6723,-9470507.5538 6112244.462,-9490351.3435 5350242.938,-10101540.0658 6092400.6723)))",
        max_error=0.0000001,
    )


##################################################################################
# Open mdb with lines layer containing a mix of single and multi-part geometries


def test_ogr_pgeo_mixed_single_multi_lines():
    pgeo_ds = ogr.Open("data/pgeo/mixed_types.mdb")

    polygon_layer = pgeo_ds.GetLayerByName("lines")
    assert polygon_layer.GetGeomType() == ogr.wkbMultiLineString

    # The PGeo format has a similar approach to multi-part handling as Shapefiles,
    # where polygon and multipolygon geometries or line and multiline geometries will
    # co-exist in a layer reported as just polygon or line type respectively.
    # To handle this in a predictable way for clients we always promote the polygon/line
    # types to multitypes, and correspondingly ALWAYS return multi polygon/line geometry
    # objects for features (even if strictly speaking the original feature had a polygon/line
    # geometry object)
    feat = polygon_layer.GetNextFeature()
    ogrtest.check_feature_geometry(
        feat,
        "MULTILINESTRING ((-10938947.9907 6608339.2042,-10244415.3516 6608339.2042))",
        max_error=0.0000001,
    )

    feat = polygon_layer.GetNextFeature()
    ogrtest.check_feature_geometry(
        feat,
        "MULTILINESTRING ((-10383321.8794 6457526.4025,-10391259.3953 5786806.3111),(-10252352.8675 6465463.9184,-9625289.1133 6469432.6764))",
        max_error=0.0000001,
    )


##################################################################################
# Open mdb with layers with z/m and check that they are handled correctly


def test_ogr_pgeo_z_m_handling():
    pgeo_ds = ogr.Open("data/pgeo/geometry_types.mdb")

    point_z_layer = pgeo_ds.GetLayerByName("point_z")
    if recent_enough_mdb_odbc_driver():
        assert point_z_layer.GetGeomType() == ogr.wkbPoint25D
    else:
        assert point_z_layer.GetGeomType() in (ogr.wkbPoint, ogr.wkbPoint25D)

    feat = point_z_layer.GetNextFeature()
    ogrtest.check_feature_geometry(feat, "POINT Z (-2 -1.0 4)", max_error=0.0000001)

    feat = point_z_layer.GetNextFeature()
    ogrtest.check_feature_geometry(feat, "POINT Z (1 2 3)", max_error=0.0000001)

    point_m_layer = pgeo_ds.GetLayerByName("point_m")
    if recent_enough_mdb_odbc_driver():
        assert point_m_layer.GetGeomType() == ogr.wkbPointM
    else:
        assert point_m_layer.GetGeomType() in (ogr.wkbPoint, ogr.wkbPointM)

    feat = point_m_layer.GetNextFeature()
    ogrtest.check_feature_geometry(feat, "POINT M (1 2 11)", max_error=0.0001)

    feat = point_m_layer.GetNextFeature()
    ogrtest.check_feature_geometry(feat, "POINT M (-2 -1 13)", max_error=0.0001)

    point_zm_layer = pgeo_ds.GetLayerByName("point_zm")
    if recent_enough_mdb_odbc_driver():
        assert point_zm_layer.GetGeomType() == ogr.wkbPointZM
    else:
        assert point_zm_layer.GetGeomType() in (ogr.wkbPoint, ogr.wkbPointZM)

    feat = point_zm_layer.GetNextFeature()
    ogrtest.check_feature_geometry(feat, "POINT ZM (-2 -1.0 4 13)", max_error=0.0001)

    feat = point_zm_layer.GetNextFeature()
    ogrtest.check_feature_geometry(feat, "POINT ZM (1 2 3 11)", max_error=0.0001)


###############################################################################


def test_ogr_pgeo_read_domains():
    ds = gdal.OpenEx("data/pgeo/domains.mdb", gdal.OF_VECTOR)
    assert set(ds.GetFieldDomainNames()) == {
        "range_domain_merge_weighted_average",
        "my coded domain",
        "my integer domain",
        "range_domain",
        "range_domain_merge_sum",
        "range_domain_spit_ratio",
        "split_policy_duplicate",
    }

    with gdal.quiet_errors():
        assert ds.GetFieldDomain("i_dont_exist") is None
    lyr = ds.GetLayer(0)
    assert lyr.GetName() == "domains"
    lyr_defn = lyr.GetLayerDefn()

    fld_defn = lyr_defn.GetFieldDefn(lyr_defn.GetFieldIndex("domain_1"))
    assert fld_defn.GetDomainName() == "my coded domain"

    domain = ds.GetFieldDomain("my coded domain")
    assert domain is not None
    assert domain.GetName() == "my coded domain"
    assert domain.GetDescription() == "domain description"
    assert domain.GetDomainType() == ogr.OFDT_CODED
    assert domain.GetFieldType() == fld_defn.GetType()
    assert domain.GetFieldSubType() == fld_defn.GetSubType()
    assert domain.GetEnumeration() == {
        "code_a": "Description A",
        "code_b": "Description B",
        "code_c": "Description C",
    }

    fld_defn = lyr_defn.GetFieldDefn(lyr_defn.GetFieldIndex("domain_3"))
    assert fld_defn.GetDomainName() == "range_domain"

    domain = ds.GetFieldDomain("range_domain")
    assert domain is not None
    assert domain.GetName() == "range_domain"
    assert domain.GetDomainType() == ogr.OFDT_RANGE
    assert domain.GetFieldType() == fld_defn.GetType()
    assert domain.GetFieldSubType() == fld_defn.GetSubType()
    assert domain.GetMinAsDouble() == -5.0
    assert domain.GetMaxAsDouble() == 50.0

    for domain_name in ds.GetFieldDomainNames():
        assert ds.GetFieldDomain(domain_name) is not None


###############################################################################
# Test retrieving layer definition


def test_ogr_pgeo_read_definition():
    ds = gdal.OpenEx("data/pgeo/metadata.mdb", gdal.OF_VECTOR)

    sql_lyr = ds.ExecuteSQL("GetLayerDefinition not a table")
    assert sql_lyr is None

    sql_lyr = ds.ExecuteSQL("GetLayerDefinition metadata")
    feat_count = sql_lyr.GetFeatureCount()
    assert feat_count == 1, "did not get expected feature count"

    feat = sql_lyr.GetNextFeature()
    assert feat is not None
    assert feat.GetField(0).startswith("<DEFeatureClassInfo")

    ds.ReleaseResultSet(sql_lyr)


###############################################################################
# Test retrieving layer metadata


def test_ogr_pgeo_read_metadata():
    ds = gdal.OpenEx("data/pgeo/metadata.mdb", gdal.OF_VECTOR)

    sql_lyr = ds.ExecuteSQL("GetLayerMetadata not a table")
    assert sql_lyr is None

    sql_lyr = ds.ExecuteSQL("GetLayerMetadata metadata")
    feat_count = sql_lyr.GetFeatureCount()
    assert feat_count == 1, "did not get expected feature count"

    feat = sql_lyr.GetNextFeature()
    assert feat is not None
    assert feat.GetField(0).startswith('<metadata xml:lang="en"><Esri>')

    ds.ReleaseResultSet(sql_lyr)


###############################################################################
# Test retrieving layer names with non-ascii characters


@pytest.mark.skipif(
    sys.platform == "win32", reason="See https://github.com/OSGeo/gdal/issues/2894"
)
def test_ogr_pgeo_layer_name_encoding():
    ds = gdal.OpenEx("data/pgeo/encoding.mdb", gdal.OF_VECTOR)

    assert ds.GetLayerCount() == 2, "did not get expected layer count"

    assert set([ds.GetLayer(i).GetName() for i in range(2)]) == set(
        ["special_chars_áõã", "táble"]
    )


###############################################################################
# Test retrieving layer names with non-ascii characters


@pytest.mark.skipif(
    sys.platform == "win32", reason="See https://github.com/OSGeo/gdal/issues/2894"
)
def test_ogr_pgeo_feature_encoding():
    ds = gdal.OpenEx("data/pgeo/encoding.mdb", gdal.OF_VECTOR)

    lyr = ds.GetLayerByName("táble")
    assert lyr is not None
    assert lyr.GetFeatureCount() == 1, "did not get expected feature count"

    feat = lyr.GetNextFeature()
    assert feat.GetField("name") == "táble"


###############################################################################
# Run test_ogrsf on dataset with non-ascii characters


def test_ogr_pgeo_ogrsf_encoding(ogrsf_path):
    ret = gdaltest.runexternal(ogrsf_path + " data/pgeo/encoding.mdb")

    assert ret.find("INFO") != -1 and ret.find("ERROR") == -1


###############################################################################
# Test reading relationships


def test_ogr_openfilegdb_read_relationships():
    # no relationships
    ds = gdal.OpenEx("data/pgeo/domains.mdb", gdal.OF_VECTOR)
    assert ds.GetRelationshipNames() is None

    # has relationships
    ds = gdal.OpenEx("data/pgeo/relationships.mdb", gdal.OF_VECTOR)
    assert set(ds.GetRelationshipNames()) == {
        "composite_one_to_many",
        "composite_one_to_one",
        "points__ATTACHREL",
        "simple_many_to_many",
        "simple_one_to_many",
        "simple_one_to_one",
    }

    assert ds.GetRelationship("xxxx") is None

    rel = ds.GetRelationship("simple_one_to_one")
    assert rel is not None
    assert rel.GetName() == "simple_one_to_one"
    assert rel.GetLeftTableName() == "table1"
    assert rel.GetRightTableName() == "table2"
    assert rel.GetMappingTableName() == ""
    assert rel.GetCardinality() == gdal.GRC_ONE_TO_ONE
    assert rel.GetType() == gdal.GRT_ASSOCIATION
    assert rel.GetLeftTableFields() == ["pk"]
    assert rel.GetRightTableFields() == ["primary_key"]
    assert rel.GetLeftMappingTableFields() is None
    assert rel.GetRightMappingTableFields() is None
    assert rel.GetForwardPathLabel() == "forward label"
    assert rel.GetBackwardPathLabel() == "backward label"
    assert rel.GetRelatedTableType() == "features"

    rel = ds.GetRelationship("simple_one_to_many")
    assert rel is not None
    assert rel.GetName() == "simple_one_to_many"
    assert rel.GetLeftTableName() == "table1"
    assert rel.GetRightTableName() == "table2"
    assert rel.GetMappingTableName() == ""
    assert rel.GetCardinality() == gdal.GRC_ONE_TO_MANY
    assert rel.GetType() == gdal.GRT_ASSOCIATION
    assert rel.GetLeftTableFields() == ["pk"]
    assert rel.GetRightTableFields() == ["primary_key"]
    assert rel.GetRelatedTableType() == "features"

    rel = ds.GetRelationship("simple_many_to_many")
    assert rel is not None
    assert rel.GetName() == "simple_many_to_many"
    assert rel.GetLeftTableName() == "table1"
    assert rel.GetRightTableName() == "table2"
    assert rel.GetMappingTableName() == "simple_many_to_many"
    assert rel.GetCardinality() == gdal.GRC_MANY_TO_MANY
    assert rel.GetType() == gdal.GRT_ASSOCIATION
    assert rel.GetLeftTableFields() == ["pk"]
    assert rel.GetLeftMappingTableFields() == ["rel_pk"]
    assert rel.GetRightTableFields() == ["primary_key"]
    assert rel.GetRightMappingTableFields() == ["rel_primary_key"]
    assert rel.GetRelatedTableType() == "features"

    rel = ds.GetRelationship("composite_one_to_one")
    assert rel is not None
    assert rel.GetName() == "composite_one_to_one"
    assert rel.GetLeftTableName() == "table3"
    assert rel.GetRightTableName() == "table4"
    assert rel.GetMappingTableName() == ""
    assert rel.GetCardinality() == gdal.GRC_ONE_TO_ONE
    assert rel.GetType() == gdal.GRT_COMPOSITE
    assert rel.GetLeftTableFields() == ["pk"]
    assert rel.GetRightTableFields() == ["primary_key"]
    assert rel.GetRelatedTableType() == "features"

    rel = ds.GetRelationship("composite_one_to_many")
    assert rel is not None
    assert rel.GetName() == "composite_one_to_many"
    assert rel.GetLeftTableName() == "table5"
    assert rel.GetRightTableName() == "table6"
    assert rel.GetMappingTableName() == ""
    assert rel.GetCardinality() == gdal.GRC_ONE_TO_MANY
    assert rel.GetType() == gdal.GRT_COMPOSITE
    assert rel.GetLeftTableFields() == ["pk"]
    assert rel.GetLeftMappingTableFields() == None
    assert rel.GetRightTableFields() == ["primary_key"]
    assert rel.GetRightMappingTableFields() == None
    assert rel.GetRelatedTableType() == "features"

    rel = ds.GetRelationship("points__ATTACHREL")
    assert rel is not None
    assert rel.GetName() == "points__ATTACHREL"
    assert rel.GetLeftTableName() == "points"
    assert rel.GetRightTableName() == "points__ATTACH"
    assert rel.GetMappingTableName() == ""
    assert rel.GetCardinality() == gdal.GRC_ONE_TO_MANY
    assert rel.GetType() == gdal.GRT_COMPOSITE
    assert rel.GetLeftTableFields() == ["OBJECTID"]
    assert rel.GetRightTableFields() == ["REL_OBJECTID"]
    assert rel.GetForwardPathLabel() == "attachment"
    assert rel.GetBackwardPathLabel() == "object"
    assert rel.GetRelatedTableType() == "media"


def test_ogr_pgeo_non_ascii(tmp_path):

    non_ascii_filename = str(tmp_path / "éé.mdb")
    shutil.copy("data/pgeo/sample.mdb", non_ascii_filename)

    pgeo_ds = ogr.Open(non_ascii_filename)
    assert pgeo_ds is not None

    assert pgeo_ds.GetLayerCount() == 4
