"""Tests for Valhalla tile lifecycle management (_tiles.py)."""

import json
import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from walkingpandas.io.match.backends._tiles import (
    build_tiles,
    ensure_tiles,
    get_cache_dir,
    pack_to_tar,
    write_valhalla_config,
)


class TestGetCacheDir:
    def test_default(self, tmp_path, monkeypatch):
        monkeypatch.delenv("WALKINGPANDAS_CACHE_DIR", raising=False)
        monkeypatch.chdir(tmp_path)
        result = get_cache_dir()
        assert result == tmp_path / "cache" / "walkingpandas" / "valhalla"

    def test_custom(self, tmp_path):
        result = get_cache_dir(tmp_path / "my_cache")
        assert result == tmp_path / "my_cache"

    def test_env_var(self, tmp_path, monkeypatch):
        monkeypatch.setenv("WALKINGPANDAS_CACHE_DIR", str(tmp_path / "env_cache"))
        result = get_cache_dir()
        assert result == tmp_path / "env_cache"


class TestWriteValhallaConfig:
    def test_writes_valid_json(self, tmp_path):
        config_path = tmp_path / "valhalla.json"
        tile_dir = tmp_path / "tiles"
        tile_dir.mkdir()

        write_valhalla_config(config_path, tile_dir)

        assert config_path.exists()
        config = json.loads(config_path.read_text())
        assert "mjolnir" in config
        assert config["mjolnir"]["tile_dir"] == str(tile_dir.resolve())
        assert config["mjolnir"]["include_pedestrian"] is True


class TestBuildTiles:
    def test_success(self, tmp_path):
        config_path = tmp_path / "valhalla.json"
        pbf_path = tmp_path / "map.osm.pbf"
        config_path.touch()
        pbf_path.touch()

        with patch("walkingpandas.io.match.backends._tiles.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            build_tiles(config_path, pbf_path)
            mock_run.assert_called_once()

    def test_failure_raises_runtime_error(self, tmp_path):
        config_path = tmp_path / "valhalla.json"
        pbf_path = tmp_path / "map.osm.pbf"

        with patch("walkingpandas.io.match.backends._tiles.subprocess.run") as mock_run:
            mock_run.side_effect = subprocess.CalledProcessError(
                1, "cmd", output="stdout", stderr="stderr"
            )
            with pytest.raises(RuntimeError, match="valhalla_build_tiles failed"):
                build_tiles(config_path, pbf_path)

    def test_not_found_raises_runtime_error(self, tmp_path):
        config_path = tmp_path / "valhalla.json"
        pbf_path = tmp_path / "map.osm.pbf"

        with patch("walkingpandas.io.match.backends._tiles.subprocess.run") as mock_run:
            mock_run.side_effect = FileNotFoundError
            with pytest.raises(RuntimeError, match="not found"):
                build_tiles(config_path, pbf_path)


class TestPackToTar:
    def test_creates_tar_with_files(self, tmp_path):
        tile_dir = tmp_path / "tiles"
        tile_dir.mkdir()
        (tile_dir / "file1.bin").write_bytes(b"data1")
        sub = tile_dir / "sub"
        sub.mkdir()
        (sub / "file2.bin").write_bytes(b"data2")

        tar_path = tmp_path / "tiles.tar"
        pack_to_tar(tile_dir, tar_path)

        assert tar_path.exists()
        import tarfile

        with tarfile.open(tar_path, "r") as tf:
            names = tf.getnames()
        assert "file1.bin" in names
        assert "sub/file2.bin" in names


class TestEnsureTiles:
    def test_validation_error_no_args(self, tmp_path):
        with pytest.raises(ValueError, match="Exactly one"):
            ensure_tiles(cache_dir=tmp_path)

    def test_validation_error_multiple_args(self, tmp_path):
        with pytest.raises(ValueError, match="Exactly one"):
            ensure_tiles(bbox=(1, 2, 3, 4), place="Zurich", cache_dir=tmp_path)

    def test_cache_hit(self, tmp_path):
        key = "abc123"
        tar = tmp_path / f"{key}.tar"
        xml = tmp_path / f"{key}.osm"
        tar.write_bytes(b"fake tar")
        xml.write_bytes(b"fake xml")

        with patch("walkingpandas.io.match.backends._tiles.generate_cache_key", return_value=key):
            result_tar, result_xml = ensure_tiles(bbox=(1, 2, 3, 4), cache_dir=tmp_path)

        assert result_tar == tar
        assert result_xml == xml

    def test_cache_hit_no_xml(self, tmp_path):
        key = "abc123"
        tar = tmp_path / f"{key}.tar"
        tar.write_bytes(b"fake tar")

        with patch("walkingpandas.io.match.backends._tiles.generate_cache_key", return_value=key):
            result_tar, result_xml = ensure_tiles(bbox=(1, 2, 3, 4), cache_dir=tmp_path)

        assert result_tar == tar
        assert result_xml is None

    def test_osm_pbf_not_found(self, tmp_path):
        with pytest.raises(FileNotFoundError, match="not found"):
            ensure_tiles(osm_pbf=tmp_path / "missing.pbf", cache_dir=tmp_path)

    @patch("walkingpandas.io.match.backends._tiles.pack_to_tar")
    @patch("walkingpandas.io.match.backends._tiles.build_tiles")
    @patch("walkingpandas.io.match.backends._tiles.write_valhalla_config")
    def test_cache_miss_with_osm_pbf(self, mock_config, mock_build, mock_pack, tmp_path):
        pbf = tmp_path / "map.osm.pbf"
        pbf.write_bytes(b"fake pbf")

        tar_path, xml_path = ensure_tiles(osm_pbf=pbf, cache_dir=tmp_path)

        assert tar_path.suffix == ".tar"
        assert xml_path is None
        mock_config.assert_called_once()
        mock_build.assert_called_once()
        mock_pack.assert_called_once()

    @patch("walkingpandas.io.match.backends._tiles.pack_to_tar")
    @patch("walkingpandas.io.match.backends._tiles.build_tiles")
    @patch("walkingpandas.io.match.backends._tiles.write_valhalla_config")
    @patch("walkingpandas.io.match.backends._tiles.download_region")
    def test_cache_miss_with_bbox(self, mock_download, mock_config, mock_build, mock_pack, tmp_path):
        tar_path, xml_path = ensure_tiles(bbox=(47.4, 47.3, 8.6, 8.5), cache_dir=tmp_path)

        assert tar_path.suffix == ".tar"
        assert xml_path is not None
        mock_download.assert_called_once()
        mock_config.assert_called_once()
        mock_build.assert_called_once()
        mock_pack.assert_called_once()

    @patch("walkingpandas.io.match.backends._tiles.pack_to_tar")
    @patch("walkingpandas.io.match.backends._tiles.build_tiles")
    @patch("walkingpandas.io.match.backends._tiles.write_valhalla_config")
    @patch("walkingpandas.io.match.backends._tiles.download_region")
    @patch("walkingpandas.io.match.backends._tiles.geocode_place_to_bbox", return_value=(47.4, 47.3, 8.6, 8.5))
    def test_cache_miss_with_place(self, mock_geocode, mock_download, mock_config, mock_build, mock_pack, tmp_path):
        tar_path, xml_path = ensure_tiles(place="Zurich", cache_dir=tmp_path)

        assert tar_path.suffix == ".tar"
        mock_geocode.assert_called_once_with("Zurich")
        mock_download.assert_called_once()
