"""Tests for the ProjectFMUDirectory class."""

import copy
import inspect
import json
import shutil
from pathlib import Path
from typing import Any
from unittest.mock import patch

import pytest
from fmu.datamodels.common.masterdata import Masterdata
from fmu.datamodels.context.mappings import (
    DataSystem,
    RelationType,
    StratigraphyIdentifierMapping,
    StratigraphyMappings,
)
from pytest import MonkeyPatch

from fmu.settings import (
    CacheResource,
    __version__,
    find_nearest_fmu_directory,
    get_fmu_directory,
)
from fmu.settings._fmu_dir import (
    FMUDirectoryBase,
    ProjectFMUDirectory,
    UserFMUDirectory,
)
from fmu.settings._readme_texts import PROJECT_README_CONTENT, USER_README_CONTENT
from fmu.settings._resources.changelog_manager import ChangelogManager
from fmu.settings._resources.lock_manager import DEFAULT_LOCK_TIMEOUT, LockManager
from fmu.settings._resources.mappings_manager import MappingsManager
from fmu.settings.models._enums import ChangeType
from fmu.settings.models.change_info import ChangeInfo
from fmu.settings.models.log import Log
from fmu.settings.models.mappings import Mappings


def test_init_existing_directory(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests initializing an ProjectFMUDirectory on an existing .fmu directory."""
    fmu = ProjectFMUDirectory(fmu_dir.base_path)
    assert fmu.path == fmu_dir.path
    assert fmu.base_path == fmu_dir.base_path


def test_get_fmu_directory(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests initializing an ProjectFMUDirectory via get_fmu_directory."""
    fmu = get_fmu_directory(fmu_dir.base_path)
    assert fmu.path == fmu_dir.path
    assert fmu.base_path == fmu_dir.base_path


def test_find_nearest_fmu_directory(
    monkeypatch: MonkeyPatch, fmu_dir: ProjectFMUDirectory
) -> None:
    """Tests initializing an ProjectFMUDirectory via find_nearest_fmu_directory."""
    subdir = fmu_dir.path / "subdir"
    subdir.mkdir()
    subdir2 = fmu_dir.path / "subdir2"
    subdir2.mkdir()
    subsubdir = subdir / "subsubdir"
    subsubdir.mkdir()

    fmu = find_nearest_fmu_directory(str(subsubdir))
    assert fmu.path == fmu_dir.path
    assert fmu.base_path == fmu_dir.base_path

    monkeypatch.chdir(fmu_dir.base_path)
    fmu = find_nearest_fmu_directory()
    assert fmu.path == fmu_dir.path
    assert fmu.base_path == fmu_dir.base_path

    monkeypatch.chdir(subdir2)
    fmu = find_nearest_fmu_directory()
    assert fmu.path == fmu_dir.path
    assert fmu.base_path == fmu_dir.base_path


def test_init_on_missing_directory(tmp_path: Path) -> None:
    """Tests initializing with a missing directory raises."""
    with pytest.raises(
        FileNotFoundError, match=f"No .fmu directory found at {tmp_path}"
    ):
        ProjectFMUDirectory(tmp_path)


def test_init_when_fmu_is_not_a_directory(tmp_path: Path) -> None:
    """Tests initialized on a .fmu non-directory raises."""
    (tmp_path / ".fmu").touch()
    with pytest.raises(
        FileExistsError, match=f".fmu exists at {tmp_path} but is not a directory"
    ):
        ProjectFMUDirectory(tmp_path)


def test_find_fmu_directory(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests find_fmu_directory method on nested children."""
    child = fmu_dir.base_path / "child"
    grand_child = child / "grandchild"
    grand_child.mkdir(parents=True)

    found_dir = ProjectFMUDirectory.find_fmu_directory(grand_child)
    assert found_dir == fmu_dir.path


def test_find_fmu_directory_not_found(tmp_path: Path) -> None:
    """Tests find_fmu_directory() returns None if no .fmu found."""
    empty_dir = tmp_path / "empty"
    empty_dir.mkdir()

    found_dir = ProjectFMUDirectory.find_fmu_directory(empty_dir)
    assert found_dir is None


def test_find_nearest(fmu_dir: ProjectFMUDirectory) -> None:
    """Test find_nearest factory method."""
    subdir = fmu_dir.base_path / "subdir"
    subdir.mkdir()

    fmu = ProjectFMUDirectory.find_nearest(subdir)
    assert fmu.path == fmu_dir.path


def test_find_nearest_not_found(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
    """Test find_nearest raises FileNotFoundError when not found."""
    monkeypatch.chdir(tmp_path)
    with pytest.raises(
        FileNotFoundError, match=f"No .fmu directory found at or above {tmp_path}"
    ):
        ProjectFMUDirectory.find_nearest()
    with pytest.raises(
        FileNotFoundError, match=f"No .fmu directory found at or above {tmp_path}"
    ):
        ProjectFMUDirectory.find_nearest(tmp_path)


def test_cache_property_returns_cached_manager(fmu_dir: ProjectFMUDirectory) -> None:
    """Cache manager should be memoized and ready for use."""
    cache = fmu_dir.cache

    assert cache is fmu_dir.cache
    assert fmu_dir._cache_manager is cache
    assert cache.max_revisions == 5  # noqa: PLR2004


def test_set_cache_max_revisions_updates_manager(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Changing retention should update the existing cache manager."""
    cache = fmu_dir.cache
    fmu_dir.cache_max_revisions = 7

    assert cache.max_revisions == 7  # noqa: PLR2004


def test_changelog_property_returns_changelog_manager(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Changelog manager should be memoized and ready for use."""
    assert isinstance(fmu_dir.changelog, ChangelogManager)
    assert fmu_dir.changelog is fmu_dir._changelog


def test_mappings_property_returns_mappings_manager(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Mappings manager should be memoized and ready for use."""
    assert isinstance(fmu_dir.mappings, MappingsManager)
    assert fmu_dir.mappings is fmu_dir._mappings


def test_get_cache_content_returns_model(fmu_dir: ProjectFMUDirectory) -> None:
    """Cache content should be returned as a validated model."""
    cached_config = fmu_dir.config.load().model_copy(update={"version": "1.2.3"})
    snapshot = fmu_dir.cache.store_revision(
        CacheResource.config.value,
        cached_config.model_dump_json(by_alias=True, indent=2),
    )
    assert snapshot is not None

    cache_content = fmu_dir.get_cache_content(
        CacheResource.config.value,
        snapshot.name,
    )

    assert cache_content.model_dump() == cached_config.model_dump()


def test_get_cache_content_raises_for_unsupported_resource(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Unsupported cache resources should raise."""
    with pytest.raises(ValueError, match="not supported for cache operations"):
        fmu_dir.get_cache_content("unsupported.json", "missing.json")


def test_restore_from_cache_restores_mappings(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Restoring mappings should refresh the resource cache."""
    cached_mapping = StratigraphyIdentifierMapping(
        source_system=DataSystem.rms,
        target_system=DataSystem.smda,
        relation_type=RelationType.primary,
        source_id="TopVolantis",
        target_id="VOLANTIS GP. Top",
    )
    cached_mappings = Mappings(stratigraphy=StratigraphyMappings(root=[cached_mapping]))
    snapshot = fmu_dir.cache.store_revision(
        CacheResource.mappings.value,
        cached_mappings.model_dump_json(by_alias=True, indent=2),
    )
    assert snapshot is not None

    current_mapping = StratigraphyIdentifierMapping(
        source_system=DataSystem.rms,
        target_system=DataSystem.smda,
        relation_type=RelationType.primary,
        source_id="TopTherys",
        target_id="THERYS GP. Top",
    )
    current_mappings = Mappings(
        stratigraphy=StratigraphyMappings(root=[current_mapping])
    )
    fmu_dir.mappings.save(current_mappings)

    fmu_dir.restore_from_cache(CacheResource.mappings.value, snapshot.name)

    assert fmu_dir.mappings._cache is not None
    assert fmu_dir.mappings._cache.model_dump() == cached_mappings.model_dump()
    restored = fmu_dir.mappings.load()
    assert restored.model_dump() == cached_mappings.model_dump()


def test_restore_from_cache_raises_for_unsupported_resource(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Unsupported cache resources should raise on restore."""
    with pytest.raises(ValueError, match="not supported for cache restoration"):
        fmu_dir.restore_from_cache("unsupported.json", "missing.json")


def test_get_config_value(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests get_config_value retrieves correctly from the config."""
    assert fmu_dir.get_config_value("version") == __version__
    assert fmu_dir.get_config_value("created_by") == "user"


def test_set_config_value(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests set_config_value sets and writes the result."""
    fmu_dir.set_config_value("version", "200.0.0")
    with open(fmu_dir.config.path, encoding="utf-8") as f:
        config_dict = json.loads(f.read())

    assert config_dict["version"] == "200.0.0"
    assert fmu_dir.get_config_value("version") == "200.0.0"
    assert fmu_dir.config.load().version == "200.0.0"


def test_update_config(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests update_config updates and saves the config for multiple values."""
    updated_config = fmu_dir.update_config({"version": "2.0.0", "created_by": "user2"})

    assert updated_config.version == "2.0.0"
    assert updated_config.created_by == "user2"

    assert fmu_dir.config.load() is not None
    assert fmu_dir.get_config_value("version", None) == "2.0.0"
    assert fmu_dir.get_config_value("created_by", None) == "user2"

    config_file = fmu_dir.config.path
    with open(config_file, encoding="utf-8") as f:
        saved_config = json.load(f)

    assert saved_config["version"] == "2.0.0"
    assert saved_config["created_by"] == "user2"


def test_update_config_invalid_data(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests that update_config raises ValidationError on bad data."""
    updates = {"version": 123}
    with pytest.raises(
        ValueError,
        match=f"Invalid value set for 'ProjectConfigManager' with updates '{updates}'",
    ):
        fmu_dir.update_config(updates)


def test_get_file_path(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests get_file_path returns correct path."""
    path = fmu_dir.get_file_path("test.txt")
    assert path == fmu_dir.path / "test.txt"


def test_file_exists(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests file_exists returns correct boolean."""
    test_file = fmu_dir.path / "exists.txt"
    test_file.touch()

    assert fmu_dir.file_exists("exists.txt") is True
    assert fmu_dir.file_exists("doesnt.txt") is False


def test_read_file(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests read_file reads bytes correctly."""
    test_file = fmu_dir.path / "bin.dat"
    test_data = b"test bin data"
    test_file.write_bytes(test_data)

    data = fmu_dir.read_file("bin.dat")
    assert data == test_data


def test_read_file_not_found(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests read_file raises FileNotFoundError for missing files."""
    with pytest.raises(FileNotFoundError, match="No such file or directory"):
        fmu_dir.read_file("not_real.txt")


def test_read_text_file(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests read_text_file reads text correctly."""
    test_file = fmu_dir.path / "text.txt"
    test_text = "test text data å"
    test_file.write_text(test_text)

    text = fmu_dir.read_text_file("text.txt")
    assert text == test_text


def test_write_text_file(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests write_text_file writes text correctly."""
    test_text = "new text data æ"
    fmu_dir.write_text_file("new_text.txt", test_text)

    file_path = fmu_dir.path / "new_text.txt"
    assert file_path.exists()
    assert file_path.read_text() == test_text


def test_write_file_creates_dir(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests write_file creates parent directories."""
    test_data = b"nested data"
    fmu_dir.write_file("nested/dir/file.dat", test_data)

    nested_dir = fmu_dir.path / "nested" / "dir"
    assert nested_dir.is_dir()

    file_path = nested_dir / "file.dat"
    assert file_path.exists()
    assert file_path.read_bytes() == test_data


def test_write_operations_raise_when_locked(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests write helpers raise when another process holds the lock."""
    lock = LockManager(fmu_dir)
    with (
        patch("socket.gethostname", return_value="other-host"),
        patch("os.getpid", return_value=12345),
    ):
        lock.acquire()

    with pytest.raises(PermissionError, match="Cannot write to .fmu directory"):
        fmu_dir.write_text_file("blocked.txt", "blocked")

    with pytest.raises(PermissionError, match="Cannot write to .fmu directory"):
        fmu_dir.write_file("blocked.bin", b"blocked")

    with (
        patch("socket.gethostname", return_value="other-host"),
        patch("os.getpid", return_value=12345),
    ):
        lock.release()


def test_list_files(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests that list_files returns the correct files."""
    (fmu_dir.path / "file1.txt").touch()
    (fmu_dir.path / "file2.txt").touch()

    subdir = fmu_dir.path / "subdir"
    subdir.mkdir()
    (subdir / "file3.txt").touch()

    files = fmu_dir.list_files()
    filenames = [f.name for f in files]

    assert "file1.txt" in filenames
    assert "file2.txt" in filenames
    assert "config.json" in filenames

    assert "file3.txt" not in filenames

    subdir_files = fmu_dir.list_files("subdir")
    assert len(subdir_files) == 1
    assert subdir_files[0].name == "file3.txt"

    not_subdir_files = fmu_dir.list_files("not_subdir")
    assert not_subdir_files == []


def test_ensure_directory(fmu_dir: ProjectFMUDirectory) -> None:
    """Tests that ensure_directory creates directories."""
    dir_path = fmu_dir.ensure_directory("nested/test/dir")
    assert dir_path.exists()
    assert dir_path.is_dir()


def test_user_init_existing_directory(user_fmu_dir: UserFMUDirectory) -> None:
    """Tests initializing an ProjectFMUDirectory on an existing .fmu directory."""
    with patch("pathlib.Path.home", return_value=user_fmu_dir.base_path):
        fmu = UserFMUDirectory()

    assert fmu.path == user_fmu_dir.path
    assert fmu.base_path == user_fmu_dir.base_path


def test_user_init_on_missing_directory(tmp_path: Path) -> None:
    """Tests initializing with a missing directory raises."""
    with (
        patch("pathlib.Path.home", return_value=tmp_path),
        pytest.raises(
            FileNotFoundError, match=f"No .fmu directory found at {tmp_path}"
        ),
    ):
        UserFMUDirectory()


def test_user_init_when_fmu_is_not_a_directory(tmp_path: Path) -> None:
    """Tests initialized on a .fmu non-directory raises."""
    (tmp_path / ".fmu").touch()
    with (
        patch("pathlib.Path.home", return_value=tmp_path),
        pytest.raises(
            FileExistsError, match=f".fmu exists at {tmp_path} but is not a directory"
        ),
    ):
        UserFMUDirectory()


def test_update_user_config(user_fmu_dir: UserFMUDirectory) -> None:
    """Tests update_config updates and saves the user config for multiple values."""
    recent_dir = "/foo/bar"
    updated_config = user_fmu_dir.update_config(
        {"version": "2.0.0", "recent_project_directories": [recent_dir]}
    )

    assert updated_config.version == "2.0.0"
    assert updated_config.recent_project_directories == [Path(recent_dir)]

    assert user_fmu_dir.config.load() is not None
    assert user_fmu_dir.get_config_value("version", None) == "2.0.0"
    assert user_fmu_dir.get_config_value("recent_project_directories") == [
        Path(recent_dir)
    ]

    config_file = user_fmu_dir.config.path
    with open(config_file, encoding="utf-8") as f:
        saved_config = json.load(f)

    assert saved_config["version"] == "2.0.0"
    assert saved_config["recent_project_directories"] == [recent_dir]


def test_update_user_config_invalid_data(user_fmu_dir: UserFMUDirectory) -> None:
    """Tests that update_config raises ValidationError on bad data."""
    updates = {"recent_project_directories": [123]}
    with pytest.raises(
        ValueError,
        match="Invalid value set for 'UserConfigManager' with updates "
        "'{'recent_project_directories':",
    ):
        user_fmu_dir.update_config(updates)


def test_update_user_config_non_unique_recent_projects(
    user_fmu_dir: UserFMUDirectory,
) -> None:
    """Tests that update_config raises on non-unique recent_project_directories."""
    updates = {"recent_project_directories": [Path("/foo/bar"), Path("/foo/bar")]}
    with pytest.raises(ValueError, match="unique entries"):
        user_fmu_dir.update_config(updates)


def test_acquire_lock_on_project_fmu(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests that a lock can be acquired on the project dir."""
    fmu_dir._lock.acquire()
    assert fmu_dir._lock.is_acquired()
    assert fmu_dir._lock.exists
    assert (fmu_dir.path / ".lock").exists()


def test_acquire_lock_on_user_fmu(
    user_fmu_dir: UserFMUDirectory,
) -> None:
    """Tests that a lock can be acquired on the user dir."""
    user_fmu_dir._lock.acquire()
    assert user_fmu_dir._lock.is_acquired()
    assert user_fmu_dir._lock.exists
    assert (user_fmu_dir.path / ".lock").exists()


def test_restore_rebuilds_project_fmu_from_cache(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests that restore should recreate missing files using cached config data."""
    fmu_dir.update_config({"version": "123.4.5"})
    cached_dump = json.loads((fmu_dir.path / "config.json").read_text())

    shutil.rmtree(fmu_dir.path)
    assert not fmu_dir.path.exists()

    fmu_dir.restore()

    assert fmu_dir.path.exists()
    readme_path = fmu_dir.path / "README"
    assert readme_path.exists()
    assert readme_path.read_text() == PROJECT_README_CONTENT

    restored_dump = json.loads((fmu_dir.path / "config.json").read_text())

    cached_dump.pop("last_modified_at", None)
    restored_dump.pop("last_modified_at", None)
    assert restored_dump == cached_dump

    cache_dir = fmu_dir.path / "cache" / "config"
    assert cache_dir.is_dir()
    assert any(cache_dir.iterdir())


def test_restore_resets_when_cache_missing(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests that restore should fall back to reset when no cached config exists."""
    fmu_dir.config._cache = None
    shutil.rmtree(fmu_dir.path)
    assert not fmu_dir.path.exists()

    with patch.object(
        fmu_dir.config, "reset", wraps=fmu_dir.config.reset
    ) as mock_reset:
        fmu_dir.restore()

    mock_reset.assert_called_once()
    assert fmu_dir.path.exists()
    readme_path = fmu_dir.path / "README"
    assert readme_path.exists()
    assert readme_path.read_text() == PROJECT_README_CONTENT
    assert (fmu_dir.config.path).exists()


def test_restore_rebuilds_user_fmu(user_fmu_dir: UserFMUDirectory) -> None:
    """Tests that user FMU restore should recreate missing files using cached state."""
    cached_dump = json.loads((user_fmu_dir.path / "config.json").read_text())

    shutil.rmtree(user_fmu_dir.path)
    assert not user_fmu_dir.path.exists()

    user_fmu_dir.restore()

    assert user_fmu_dir.path.exists()
    readme_path = user_fmu_dir.path / "README"
    assert readme_path.exists()
    assert readme_path.read_text() == USER_README_CONTENT

    restored_dump = json.loads((user_fmu_dir.path / "config.json").read_text())

    cached_dump.pop("last_modified_at", None)
    restored_dump.pop("last_modified_at", None)
    assert restored_dump == cached_dump


def test_fmu_directory_base_exposes_lock_timeout_kwarg() -> None:
    """Tests that the kw-only lock timeout argument remains available."""
    signature = inspect.signature(FMUDirectoryBase.__init__)
    lock_timeout = signature.parameters.get("lock_timeout_seconds")

    assert lock_timeout is not None, "lock_timeout_seconds kwarg missing from base init"
    assert lock_timeout.kind is inspect.Parameter.KEYWORD_ONLY
    assert lock_timeout.default == DEFAULT_LOCK_TIMEOUT


def test_find_rms_projects_none_found(fmu_dir: ProjectFMUDirectory) -> None:
    """Test finding RMS projects when none exist."""
    projects = fmu_dir.find_rms_projects()
    assert projects == []


def test_find_rms_projects_single(fmu_dir: ProjectFMUDirectory) -> None:
    """Test finding a single RMS project."""
    rms_project = fmu_dir.base_path / "rms" / "model" / "test.rms"
    rms_project.mkdir(parents=True)
    (rms_project / ".master").write_text("master content")
    (rms_project / "rms.ini").write_text("[RMS]")

    projects = fmu_dir.find_rms_projects()

    assert len(projects) == 1
    assert projects[0] == rms_project


def test_find_rms_projects_multiple(fmu_dir: ProjectFMUDirectory) -> None:
    """Test finding multiple RMS projects."""
    rms_project_1 = fmu_dir.base_path / "rms" / "model" / "drogon.rms14.2.2"
    rms_project_1.mkdir(parents=True)
    (rms_project_1 / ".master").write_text("master content")
    (rms_project_1 / "rms.ini").write_text("[RMS]")

    rms_project_2 = fmu_dir.base_path / "rms" / "model" / "drogon.rms14.1.0"
    rms_project_2.mkdir(parents=True)
    (rms_project_2 / ".master").write_text("master content")
    (rms_project_2 / "rms.ini").write_text("[RMS]")

    projects = fmu_dir.find_rms_projects()

    assert len(projects) == 2  # noqa: PLR2004
    assert set(projects) == {rms_project_1, rms_project_2}


def test_find_rms_projects_incomplete_missing_master(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Test that projects missing .master file are not included."""
    incomplete = fmu_dir.base_path / "rms" / "model" / "incomplete"
    incomplete.mkdir(parents=True)
    (incomplete / "rms.ini").write_text("[RMS]")

    projects = fmu_dir.find_rms_projects()
    assert projects == []


def test_find_rms_projects_incomplete_missing_ini(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Test that projects missing rms.ini file are not included."""
    incomplete = fmu_dir.base_path / "rms" / "model" / "incomplete"
    incomplete.mkdir(parents=True)
    (incomplete / ".master").write_text("master content")

    projects = fmu_dir.find_rms_projects()
    assert projects == []


def test_find_rms_projects_ignores_files_in_model_dir(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Test that extra files in rms/model do not affect detection."""
    model_dir = fmu_dir.base_path / "rms" / "model"
    model_dir.mkdir(parents=True)
    (model_dir / "not_a_directory.txt").write_text("some file")
    rms_project = model_dir / "valid.rms"
    rms_project.mkdir()
    (rms_project / ".master").write_text("content")
    (rms_project / "rms.ini").write_text("[RMS]")

    projects = fmu_dir.find_rms_projects()

    assert len(projects) == 1
    assert projects[0] == rms_project


def test_find_rms_projects_ignores_rms_model_as_file(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Test that rms/model path that is a file is ignored."""
    rms_dir = fmu_dir.base_path / "rms"
    rms_dir.mkdir()
    (rms_dir / "model").write_text("not a directory")

    project = fmu_dir.find_rms_projects()

    assert len(project) == 0


def test_find_rms_projects_ignores_model_directory_in_subfolders_of_project_root(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Test that rms/model under subfolders of the root is ignored."""
    invalid_model_location = fmu_dir.base_path / "subfolder" / "rms" / "model"
    invalid_model_location.mkdir(parents=True)
    (invalid_model_location / ".master").write_text("content")
    (invalid_model_location / "rms.ini").write_text("[RMS]")

    projects = fmu_dir.find_rms_projects()

    assert len(projects) == 0


def test_find_rms_projects_with_config_path(
    fmu_dir: ProjectFMUDirectory,
) -> None:
    """Test storing and retrieving RMS project path in config."""
    rms_project = fmu_dir.base_path / "rms" / "model" / "my.rms"
    rms_project.mkdir(parents=True)
    (rms_project / ".master").write_text("content")
    (rms_project / "rms.ini").write_text("[RMS]")

    fmu_dir.set_config_value("rms", {"path": str(rms_project), "version": "14.2.2"})

    stored_rms = fmu_dir.get_config_value("rms")
    assert stored_rms.path == rms_project

    fmu_dir2 = ProjectFMUDirectory(fmu_dir.base_path)
    assert fmu_dir2.get_config_value("rms").path == rms_project


def test_fmu_directory_base_get_dir_diff_with_other_fmu_dir(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
    masterdata_dict: dict[str, Any],
) -> None:
    """Tests getting the resource diff with another .fmu directory."""
    new_fmu_dir = extra_fmu_dir
    new_fmu_dir.set_config_value("masterdata", masterdata_dict)
    diff_fmu_dir = fmu_dir.get_dir_diff(new_fmu_dir)

    assert len(diff_fmu_dir) == 1
    assert "config" in diff_fmu_dir
    assert len(diff_fmu_dir["config"]) == 1
    assert diff_fmu_dir["config"][0][0] == "masterdata"
    assert diff_fmu_dir["config"][0][1] is None
    assert diff_fmu_dir["config"][0][2] == Masterdata.model_validate(masterdata_dict)


def test_fmu_directory_base_get_dir_diff_only_diff_whitelisted_resources(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
    masterdata_dict: dict[str, Any],
) -> None:
    """Tests that only the whitelisted resources are diffed."""
    new_fmu_dir = extra_fmu_dir
    new_fmu_dir.set_config_value("masterdata", masterdata_dict)
    new_fmu_dir._lock.acquire()

    assert new_fmu_dir._changelog.exists
    assert new_fmu_dir._lock.exists
    assert not fmu_dir._changelog.exists
    assert not fmu_dir._lock.exists

    diff_fmu_dir = fmu_dir.get_dir_diff(new_fmu_dir)

    assert len(diff_fmu_dir) == 1
    assert diff_fmu_dir["config"]
    assert "_lock" not in diff_fmu_dir


def test_fmu_directory_base_get_dir_diff_skip_when_resource_not_exist(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests that sync is skipped for resources that does not exist.

    When a resource does not exist in one of the .fmu directories,
    sync is skipped for that resource.
    """
    new_fmu_dir = extra_fmu_dir
    new_fmu_dir.config.path.unlink()

    diff_fmu_dir = fmu_dir.get_dir_diff(new_fmu_dir)
    assert len(diff_fmu_dir) == 0


def test_fmu_directory_base_sync_dir_with_other_fmu_dir(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
    masterdata_dict: dict[str, Any],
) -> None:
    """Tests syncing resources with another .fmu directory."""
    new_fmu_dir = extra_fmu_dir
    new_fmu_dir.set_config_value("masterdata", masterdata_dict)

    assert new_fmu_dir.config.load().masterdata
    assert not fmu_dir.config.load().masterdata

    updated_resources = fmu_dir.sync_dir(new_fmu_dir)

    assert len(updated_resources) == 1
    assert "config" in updated_resources
    assert updated_resources["config"].masterdata == Masterdata.model_validate(
        masterdata_dict
    )
    assert new_fmu_dir.config.load().masterdata == fmu_dir.config.load().masterdata


def test_fmu_directory_base_sync_dir_only_whitelisted_resources(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
    masterdata_dict: dict[str, Any],
) -> None:
    """Tests that only the whitelisted resources are synced."""
    new_fmu_dir = extra_fmu_dir
    new_fmu_dir.set_config_value("masterdata", masterdata_dict)
    new_fmu_dir._lock.acquire()

    assert new_fmu_dir._changelog.exists
    assert new_fmu_dir._lock.exists
    assert not fmu_dir._changelog.exists
    assert not fmu_dir._lock.exists

    updates = fmu_dir.sync_dir(new_fmu_dir)

    assert "_lock" not in updates
    assert len(updates) == 1
    assert updates["config"]
    assert not fmu_dir._lock.exists


def test_fmu_directory_base_sync_dir_skip_when_resource_not_exist(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests that sync is skipped for resources that does not exist.

    When a resource does not exist in one of the .fmu directories,
    sync is skipped for that resource.
    """
    new_fmu_dir = extra_fmu_dir
    new_fmu_dir.config.path.unlink()
    updates = fmu_dir.sync_dir(new_fmu_dir)
    assert len(updates) == 0


def test_fmu_directory_base_sync_dir_skip_when_no_diff(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests that sync is skipped for resources with no diff.

    When doing a sync of two equal resources, sync is skipped for that resource.
    """
    new_fmu_dir = extra_fmu_dir
    assert fmu_dir.config.exists
    assert new_fmu_dir.config.exists
    assert fmu_dir.config.load() == new_fmu_dir.config.load()

    updates = fmu_dir.sync_dir(new_fmu_dir)
    assert len(updates) == 0


def test_fmu_directory_base_sync_runtime_variables(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests that runtime variables are synced as part of .fmu directory sync.

    When resources in the .fmu directory are synced,
    runtime variables should be synced accordingly.
    """
    new_fmu_dir = extra_fmu_dir
    new_cache_max_revisions = 10
    old_cache_max_revisions = fmu_dir.config.load().cache_max_revisions
    assert fmu_dir.cache_max_revisions != new_cache_max_revisions
    assert fmu_dir.cache_max_revisions == fmu_dir.config.load().cache_max_revisions

    new_config = new_fmu_dir.config.load()
    new_config.cache_max_revisions = new_cache_max_revisions
    new_fmu_dir.config.save(new_config)

    updates = fmu_dir.sync_dir(new_fmu_dir)
    assert "config" in updates
    assert updates["config"].cache_max_revisions == new_cache_max_revisions
    assert fmu_dir.cache_max_revisions == new_cache_max_revisions
    assert fmu_dir.cache.max_revisions == new_cache_max_revisions

    config = fmu_dir.config.load()
    assert config.cache_max_revisions is new_cache_max_revisions
    config.cache_max_revisions = old_cache_max_revisions
    fmu_dir.config.save(config)
    assert (
        fmu_dir.config.load(force=True).cache_max_revisions == old_cache_max_revisions
    )
    assert fmu_dir.cache_max_revisions == new_cache_max_revisions
    assert fmu_dir.cache.max_revisions == new_cache_max_revisions

    fmu_dir._sync_runtime_variables()

    assert fmu_dir.cache_max_revisions == old_cache_max_revisions
    assert fmu_dir.cache.max_revisions == old_cache_max_revisions


def test_fmu_directory_base_get_dir_diff_with_same_changelog(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests getting the changelog diff with equivalent directories."""
    log_entry = ChangeInfo(
        change_type=ChangeType.add,
        user="fmu_dir_user",
        path=Path("/some_path"),
        change="some change",
        hostname="host",
        file="config",
        key="test_key",
    )
    fmu_dir._changelog.add_log_entry(log_entry)
    extra_fmu_dir._changelog.add_log_entry(log_entry)
    dir_diff = fmu_dir.get_dir_diff(extra_fmu_dir)

    for resource, change_list in dir_diff.items():
        assert len(change_list) == 0, f"Resource {resource} has changes"


def test_fmu_directory_base_get_dir_diff_with_changelog(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests getting the changelog diff with another .fmu directory."""
    log_entry = ChangeInfo(
        change_type=ChangeType.add,
        user="fmu_dir_user",
        path=Path("/some_path"),
        change="some change",
        hostname="host",
        file="config",
        key="test_key",
    )
    fmu_dir._changelog.add_log_entry(log_entry)
    assert len(fmu_dir._changelog.load()) == 1
    assert fmu_dir._changelog.load()[0] == log_entry

    new_fmu_dir = extra_fmu_dir
    new_log_entry = ChangeInfo(
        change_type=ChangeType.add,
        user="new_fmu_dir_user",
        path=Path("/some_new_path"),
        change="new change",
        hostname="new_host",
        file="config",
        key="new_test_key",
    )
    new_fmu_dir._changelog.add_log_entry(new_log_entry)
    assert len(new_fmu_dir._changelog.load()) == 1
    assert new_fmu_dir._changelog.load()[0] == new_log_entry

    dir_diff = fmu_dir.get_dir_diff(new_fmu_dir)

    expected_no_of_resources = 2
    assert len(dir_diff) == expected_no_of_resources
    assert dir_diff["config"] == []
    assert "_changelog" in dir_diff
    changelog_diff = dir_diff["_changelog"]

    assert len(changelog_diff) == 1
    assert changelog_diff[0][0] == "changelog"
    assert changelog_diff[0][1] is None
    assert isinstance(changelog_diff[0][2], Log)
    assert len(changelog_diff[0][2]) == 1
    assert changelog_diff[0][2].root[0] == new_log_entry


def test_fmu_directory_base_sync_dir_with_changelog(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests syncing changelog with another .fmu directory."""
    log_entry = ChangeInfo(
        change_type=ChangeType.add,
        user="fmu_dir_user",
        path=Path("/some_path"),
        change="some change",
        hostname="host",
        file="config",
        key="test_key",
    )
    fmu_dir._changelog.add_log_entry(log_entry)
    assert len(fmu_dir._changelog.load()) == 1
    assert fmu_dir._changelog.load()[0] == log_entry

    new_fmu_dir = extra_fmu_dir
    new_log_entry = ChangeInfo(
        change_type=ChangeType.add,
        user="new_fmu_dir_user",
        path=Path("/some_new_path"),
        change="new change",
        hostname="new_host",
        file="config",
        key="new_test_key",
    )
    new_fmu_dir._changelog.add_log_entry(new_log_entry)
    assert len(new_fmu_dir._changelog.load()) == 1
    assert new_fmu_dir._changelog.load()[0] == new_log_entry

    updated_resources = fmu_dir.sync_dir(new_fmu_dir)

    assert len(updated_resources) == 1
    assert "_changelog" in updated_resources
    updated_changelog: Log[ChangeInfo] = updated_resources["_changelog"]
    expected_log_length = 3
    assert len(updated_changelog) == expected_log_length
    assert updated_changelog[0] == log_entry
    assert updated_changelog[1] == new_log_entry
    assert updated_changelog[2].change_type == ChangeType.merge
    assert "_changelog" in updated_changelog[2].change
    assert updated_changelog[2].path == fmu_dir.path


def test_fmu_directory_base_sync_dir_with_all_resources(
    fmu_dir: ProjectFMUDirectory,
    masterdata_dict: dict[str, Any],
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests syncing with another .fmu directory with config, mappings and changelog."""
    assert fmu_dir.config.load().masterdata is None
    fmu_dir.set_config_value("masterdata", masterdata_dict)
    assert fmu_dir.config.load().masterdata is not None
    assert len(fmu_dir._changelog.load()) == 1
    assert fmu_dir._changelog.load()[0].change_type == ChangeType.update

    fmu_dir._mappings.update_stratigraphy_mappings(StratigraphyMappings(root=[]))
    new_fmu_dir = extra_fmu_dir
    new_strat_mapping = StratigraphyIdentifierMapping(
        source_system=DataSystem.rms,
        target_system=DataSystem.smda,
        relation_type=RelationType.primary,
        source_id="TopViking",
        target_id="VIKING GP. Top",
    )
    new_fmu_dir._mappings.update_stratigraphy_mappings(
        StratigraphyMappings(root=[new_strat_mapping])
    )

    assert len(new_fmu_dir._changelog.load()) == 1
    assert new_fmu_dir._changelog.load()[0].key == "stratigraphy"
    assert new_fmu_dir._changelog.load()[0].file == "mappings.json"

    updated_resources = fmu_dir.sync_dir(new_fmu_dir)

    expected_no_of_resources = 3
    assert len(updated_resources) == expected_no_of_resources
    assert "config" in updated_resources
    assert fmu_dir.config.load().masterdata is None

    assert "_mappings" in updated_resources
    assert len(fmu_dir._mappings.well_mappings) == 0
    assert len(fmu_dir._mappings.stratigraphy_mappings) == 1
    assert fmu_dir._mappings.stratigraphy_mappings[0] == new_strat_mapping

    assert "_changelog" in updated_resources
    updated_changelog: Log[ChangeInfo] = updated_resources["_changelog"]
    expected_log_length = 6

    # Assert existing entries
    assert len(updated_changelog) == expected_log_length
    assert updated_changelog[0].key == "masterdata"
    assert updated_changelog[0].path == fmu_dir.path
    assert updated_changelog[1].key == "stratigraphy"
    assert updated_changelog[1].path == fmu_dir.path

    # Assert merged entry
    assert updated_changelog[2].key == "stratigraphy"
    assert updated_changelog[2].path == new_fmu_dir.path
    assert updated_changelog[2] == new_fmu_dir._changelog.load()[0]

    # Assert entries from merging config and mappings
    assert updated_changelog[3].key == "masterdata"
    assert updated_changelog[3].path == fmu_dir.path
    assert updated_changelog[4].key == "stratigraphy"
    assert updated_changelog[4].path == fmu_dir.path

    # Assert log entry with merge details
    assert updated_changelog[5].change_type == ChangeType.merge
    assert "config" in updated_changelog[5].file
    assert "_changelog" in updated_changelog[5].file
    assert "_mappings" in updated_changelog[5].file


def test_fmu_directory_base_sync_dir_dont_sync_ignored_fields(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests that fields to be ignored are not synced.

    When syncing two .fmu directories, Pydantic resources with fields set
    to be ignored in a diff, should not be synced.
    """
    fmu_dir.set_config_value("cache_max_revisions", 5)
    created_at = fmu_dir.config.load().created_at
    created_by = fmu_dir.config.load().created_by
    last_modified_at = fmu_dir.config.load().last_modified_at
    last_modified_by = fmu_dir.config.load().last_modified_by

    new_fmu_dir = extra_fmu_dir
    new_cache_max_revisions = 10
    new_fmu_dir.set_config_value("cache_max_revisions", new_cache_max_revisions)

    updates = fmu_dir.sync_dir(new_fmu_dir)

    assert updates["config"].cache_max_revisions == new_cache_max_revisions

    # created_at and created_by should never change
    assert updates["config"].created_at == created_at
    assert updates["config"].created_by == created_by

    # last_modified_at changes when the config has been updated through the sync
    assert updates["config"].last_modified_at != last_modified_at
    assert updates["config"].last_modified_by == last_modified_by

    expected_log_length = 4
    assert len(updates["_changelog"]) == expected_log_length

    # First entry should be the cache_max_revision update in fmu_dir
    assert updates["_changelog"][0].key == "cache_max_revisions"
    assert "Old value: 5 -> New value: 5" in updates["_changelog"][0].change

    # Second entry should be the cache_max_revision update from the changelog merge
    assert updates["_changelog"][1].key == "cache_max_revisions"
    assert "Old value: 5 -> New value: 10" in updates["_changelog"][1].change
    assert updates["_changelog"][1].path == new_fmu_dir.path

    # Third entry should be the cache_max_revision update from the config merge
    assert updates["_changelog"][2].key == "cache_max_revisions"
    assert "Old value: 5 -> New value: 10" in updates["_changelog"][2].change
    assert updates["_changelog"][2].path == fmu_dir.path

    # Fourth entry should be the logged merge details
    assert updates["_changelog"][3].key == ".fmu"
    assert updates["_changelog"][3].change_type == ChangeType.merge

    # This should not happen, but just to illustrate:
    # Force updating one of the diff ignore fields, adds log entries that will be merged
    new_fmu_dir.set_config_value("created_by", "johndoe")
    updates = fmu_dir.sync_dir(new_fmu_dir)

    # The updated created_by value should not be merged
    assert new_fmu_dir.config.load().created_by == "johndoe"
    assert "config" not in updates
    assert fmu_dir.config.load().created_by != "johndoe"

    # The changelog entry for the update will be merged
    assert updates["_changelog"][4].key == "created_by"
    assert "Old value: user -> New value: johndoe" in updates["_changelog"][4].change


def test_fmu_directory_base_get_dir_diff_with_mappings(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests getting the diff with another .fmu directory with mappings."""
    strat_mapping = StratigraphyIdentifierMapping(
        source_system=DataSystem.rms,
        target_system=DataSystem.smda,
        relation_type=RelationType.primary,
        source_id="TopViking",
        target_id="VIKING GP. Top",
    )
    fmu_dir._mappings.update_stratigraphy_mappings(
        StratigraphyMappings(root=[strat_mapping])
    )

    new_fmu_dir = extra_fmu_dir
    new_strat_mapping = copy.deepcopy(strat_mapping)
    new_strat_mapping.source_id = "TopVolantis"
    new_strat_mapping.target_id = "VOLANTIS GP. Top"
    new_fmu_dir._mappings.update_stratigraphy_mappings(
        StratigraphyMappings(root=[new_strat_mapping])
    )

    old_fmu_dir = copy.deepcopy(fmu_dir)
    dir_diff = fmu_dir.get_dir_diff(new_fmu_dir)

    # Assert no changes made to the .fmu directory
    assert fmu_dir.config.load() == old_fmu_dir.config.load()
    assert fmu_dir._changelog.load() == old_fmu_dir._changelog.load()
    assert fmu_dir._mappings.load() == old_fmu_dir._mappings.load()

    # Assert mappings diff is as expected
    assert "_mappings" in dir_diff
    assert len(dir_diff["_mappings"]) == 1
    mappings_diff = dir_diff["_mappings"][0]
    assert mappings_diff[0] == "mappings"
    assert mappings_diff[1] is None
    assert isinstance(mappings_diff[2], Mappings)
    assert len(mappings_diff[2].wells) == 0
    assert len(mappings_diff[2].stratigraphy) == 1
    assert mappings_diff[2].stratigraphy[0] == new_strat_mapping

    # Assert no config changes
    assert "config" in dir_diff
    assert len(dir_diff["config"]) == 0

    # Assert changelog diff has only the mappings update entry
    assert len(dir_diff["_changelog"]) == 1
    changelog_diff = dir_diff["_changelog"][0]
    assert changelog_diff[0] == "changelog"
    assert changelog_diff[1] is None
    assert len(changelog_diff[2]) == 1
    assert changelog_diff[2][0].key == "stratigraphy"
    assert changelog_diff[2][0].file == "mappings.json"


def test_fmu_directory_base_sync_dir_with_mappings(
    fmu_dir: ProjectFMUDirectory,
    extra_fmu_dir: ProjectFMUDirectory,
) -> None:
    """Tests syncing mappings resource with another .fmu directory."""
    strat_mapping = StratigraphyIdentifierMapping(
        source_system=DataSystem.rms,
        target_system=DataSystem.smda,
        relation_type=RelationType.primary,
        source_id="TopViking",
        target_id="VIKING GP. Top",
    )
    fmu_dir._mappings.update_stratigraphy_mappings(
        StratigraphyMappings(root=[strat_mapping])
    )

    new_fmu_dir = extra_fmu_dir
    new_strat_mapping = copy.deepcopy(strat_mapping)
    new_strat_mapping.source_id = "TopVolantis"
    new_strat_mapping.target_id = "VOLANTIS GP. Top"
    new_fmu_dir._mappings.update_stratigraphy_mappings(
        StratigraphyMappings(root=[new_strat_mapping])
    )

    updates = fmu_dir.sync_dir(new_fmu_dir)

    # Assert mappings are updated as expected
    assert "_mappings" in updates
    assert updates["_mappings"] == fmu_dir._mappings.load()
    assert len(fmu_dir._mappings.well_mappings) == 0
    assert len(fmu_dir._mappings.stratigraphy_mappings) == 1
    assert fmu_dir._mappings.stratigraphy_mappings[0] == new_strat_mapping

    # Assert no config changes
    assert "config" not in updates

    # Assert changelog has all the mapping updates plus the merge entry
    changelog: Log[ChangeInfo] = updates["_changelog"]
    expected_log_length = 4
    assert len(changelog) == expected_log_length
    assert changelog[0].path == fmu_dir.path
    assert changelog[0].key == "stratigraphy"
    assert changelog[1].path == new_fmu_dir.path
    assert changelog[1].key == "stratigraphy"
    assert changelog[2].path == fmu_dir.path
    assert changelog[2].key == "stratigraphy"
    assert changelog[3].path == fmu_dir.path
    assert changelog[3].change_type == ChangeType.merge
    assert "mappings" in changelog[3].file
