"""Tests for file storage implementation."""

import json
from pathlib import Path
from unittest.mock import patch

import pytest

from local_deep_research.storage.file import FileReportStorage


class TestFileReportStorageInit:
    """Tests for FileReportStorage initialization."""

    def test_uses_default_directory_when_none(self, tmp_path):
        """Should use default research outputs directory when none provided."""
        with patch(
            "local_deep_research.storage.file.get_research_outputs_directory",
            return_value=tmp_path,
        ):
            storage = FileReportStorage()
            assert storage.base_dir == tmp_path

    def test_uses_provided_directory(self, tmp_path):
        """Should use provided directory."""
        custom_dir = tmp_path / "custom"
        storage = FileReportStorage(base_dir=custom_dir)
        assert storage.base_dir == custom_dir

    def test_creates_directory_if_not_exists(self, tmp_path):
        """Should create directory if it doesn't exist."""
        new_dir = tmp_path / "new_reports"
        storage = FileReportStorage(base_dir=new_dir)
        assert storage.base_dir.exists()


class TestGetReportPath:
    """Tests for _get_report_path method."""

    def test_returns_md_file_path(self, tmp_path):
        """Should return path with .md extension."""
        storage = FileReportStorage(base_dir=tmp_path)
        path = storage._get_report_path("test-uuid")
        assert path == tmp_path / "test-uuid.md"

    def test_returns_path_object(self, tmp_path):
        """Should return Path object."""
        storage = FileReportStorage(base_dir=tmp_path)
        path = storage._get_report_path("test-uuid")
        assert isinstance(path, Path)


class TestGetMetadataPath:
    """Tests for _get_metadata_path method."""

    def test_returns_metadata_json_path(self, tmp_path):
        """Should return path with _metadata.json suffix."""
        storage = FileReportStorage(base_dir=tmp_path)
        path = storage._get_metadata_path("test-uuid")
        assert path == tmp_path / "test-uuid_metadata.json"


class TestPathTraversalContainment:
    """Regression tests for the #3090 traversal guard: a research_id that
    escapes base_dir must be rejected, not resolved to an arbitrary on-disk
    path. (research_ids are normally server-generated UUIDs; this is
    defense-in-depth.)"""

    def test_report_path_rejects_parent_traversal(self, tmp_path):
        storage = FileReportStorage(base_dir=tmp_path)
        with pytest.raises(ValueError, match="Path traversal attempt"):
            storage._get_report_path("../../etc/passwd")

    def test_metadata_path_rejects_parent_traversal(self, tmp_path):
        storage = FileReportStorage(base_dir=tmp_path)
        with pytest.raises(ValueError, match="Path traversal attempt"):
            storage._get_metadata_path("../../etc/passwd")

    def test_report_path_rejects_single_level_escape(self, tmp_path):
        storage = FileReportStorage(base_dir=tmp_path / "reports")
        with pytest.raises(ValueError, match="Path traversal attempt"):
            storage._get_report_path("../escapee")

    def test_normal_id_stays_within_base_dir(self, tmp_path):
        storage = FileReportStorage(base_dir=tmp_path)
        path = storage._get_report_path("9f8e-uuid-1234")
        assert path.is_relative_to(tmp_path.resolve())
        assert path.name == "9f8e-uuid-1234.md"


class TestSaveReport:
    """Tests for save_report method."""

    def test_saves_report_content(self, tmp_path, sample_report_content):
        """Should save report content to file."""
        storage = FileReportStorage(base_dir=tmp_path)

        # Patch at source location since it's imported inside the method
        with patch(
            "local_deep_research.security.file_write_verifier.write_file_verified"
        ) as mock_write:
            with patch(
                "local_deep_research.security.file_write_verifier.write_json_verified"
            ):
                result = storage.save_report("test-uuid", sample_report_content)

        assert result is True
        mock_write.assert_called_once()

    def test_saves_metadata_when_provided(
        self, tmp_path, sample_report_content, sample_metadata
    ):
        """Should save metadata when provided."""
        storage = FileReportStorage(base_dir=tmp_path)

        with patch(
            "local_deep_research.security.file_write_verifier.write_file_verified"
        ):
            with patch(
                "local_deep_research.security.file_write_verifier.write_json_verified"
            ) as mock_json:
                storage.save_report(
                    "test-uuid", sample_report_content, metadata=sample_metadata
                )

        mock_json.assert_called_once()

    def test_skips_metadata_when_not_provided(
        self, tmp_path, sample_report_content
    ):
        """Should skip metadata save when not provided."""
        storage = FileReportStorage(base_dir=tmp_path)

        with patch(
            "local_deep_research.security.file_write_verifier.write_file_verified"
        ):
            with patch(
                "local_deep_research.security.file_write_verifier.write_json_verified"
            ) as mock_json:
                storage.save_report("test-uuid", sample_report_content)

        mock_json.assert_not_called()

    def test_returns_false_on_error(self, tmp_path, sample_report_content):
        """Should return False on error."""
        storage = FileReportStorage(base_dir=tmp_path)

        with patch(
            "local_deep_research.security.file_write_verifier.write_file_verified",
            side_effect=Exception("write error"),
        ):
            result = storage.save_report("test-uuid", sample_report_content)

        assert result is False

    def test_save_report_gates_on_report_enable_file_backup(
        self, tmp_path, sample_report_content
    ):
        """Regression: the verifier gate must consult the same setting key
        that the UI toggle writes (``report.enable_file_backup``). The
        previous literal ``storage.allow_file_backup`` was never registered
        in ``default_settings.json``, so every save raised
        ``FileWriteSecurityError`` regardless of the user's toggle.
        """
        from local_deep_research.security.file_write_verifier import (
            FileWriteSecurityError,
        )

        captured = {}

        def fake_write_file_verified(path, content, setting_name, **_kwargs):
            captured["setting_name"] = setting_name
            raise FileWriteSecurityError("ignored")

        storage = FileReportStorage(base_dir=tmp_path)

        with patch(
            "local_deep_research.security.file_write_verifier.write_file_verified",
            fake_write_file_verified,
        ):
            result = storage.save_report("abc", sample_report_content)

        # The exception is swallowed by FileReportStorage.save_report, so the
        # outer result is False -- the bug we care about is the *key* passed
        # to the verifier, not the return value.
        assert result is False
        assert captured["setting_name"] == "report.enable_file_backup"

    def test_save_metadata_gates_on_report_enable_file_backup(
        self, tmp_path, sample_report_content, sample_metadata
    ):
        """The metadata write must consult the same UI-visible key as the
        body write. A divergence here would silently drop the
        ``_metadata.json`` sidecar even when the body lands on disk.
        """
        from local_deep_research.security.file_write_verifier import (
            FileWriteSecurityError,
        )

        captured = {}

        def fake_write_file_verified(path, content, setting_name, **_kwargs):
            # Allow the body write through so the metadata branch runs.
            captured.setdefault("body_setting_name", setting_name)

        def fake_write_json_verified(path, payload, setting_name, **_kwargs):
            captured["metadata_setting_name"] = setting_name
            raise FileWriteSecurityError("ignored")

        storage = FileReportStorage(base_dir=tmp_path)

        with (
            patch(
                "local_deep_research.security.file_write_verifier.write_file_verified",
                fake_write_file_verified,
            ),
            patch(
                "local_deep_research.security.file_write_verifier.write_json_verified",
                fake_write_json_verified,
            ),
        ):
            storage.save_report(
                "abc", sample_report_content, metadata=sample_metadata
            )

        assert captured["body_setting_name"] == "report.enable_file_backup"
        assert captured["metadata_setting_name"] == "report.enable_file_backup"

    def test_save_report_and_factory_agree_on_setting_key(
        self, tmp_path, sample_report_content, mock_session
    ):
        """End-to-end key-alignment check: the key consulted by
        ``FileReportStorage.save_report`` must match the key consulted by
        ``get_report_storage``. If either side drifts, a user who flipped
        the UI toggle would still see no files on disk.
        """
        from local_deep_research.storage.factory import get_report_storage

        factory_calls = []

        def fake_get_setting(name, settings_snapshot=None):
            factory_calls.append(name)
            return True

        file_calls = []

        from local_deep_research.security.file_write_verifier import (
            FileWriteSecurityError,
        )

        def fake_write_file_verified(path, content, setting_name, **_kwargs):
            file_calls.append(setting_name)
            raise FileWriteSecurityError("ignored")

        with (
            patch(
                "local_deep_research.storage.factory.get_setting_from_snapshot",
                fake_get_setting,
            ),
            patch(
                "local_deep_research.security.file_write_verifier.write_file_verified",
                fake_write_file_verified,
            ),
        ):
            # Factory side: which key unlocks file backup?
            factory = get_report_storage(session=mock_session)
            assert factory.enable_file_storage is True

            # Storage side: which key gates the disk write?
            fs = FileReportStorage(base_dir=tmp_path)
            fs.save_report("abc", sample_report_content)

        assert factory_calls == ["report.enable_file_backup"]
        assert file_calls == ["report.enable_file_backup"]
        assert factory_calls == file_calls


class TestGetReport:
    """Tests for get_report method."""

    def test_returns_content_when_file_exists(
        self, tmp_path, sample_report_content
    ):
        """Should return content when file exists."""
        storage = FileReportStorage(base_dir=tmp_path)
        report_path = tmp_path / "test-uuid.md"
        report_path.write_text(sample_report_content, encoding="utf-8")

        result = storage.get_report("test-uuid")

        assert result == sample_report_content

    def test_returns_none_when_file_not_exists(self, tmp_path):
        """Should return None when file doesn't exist."""
        storage = FileReportStorage(base_dir=tmp_path)

        result = storage.get_report("nonexistent-uuid")

        assert result is None

    def test_returns_none_on_error(self, tmp_path):
        """Should return None on read error."""
        storage = FileReportStorage(base_dir=tmp_path)
        report_path = tmp_path / "test-uuid.md"
        report_path.write_text("content", encoding="utf-8")

        with patch("builtins.open", side_effect=Exception("read error")):
            result = storage.get_report("test-uuid")

        assert result is None


class TestGetReportWithMetadata:
    """Tests for get_report_with_metadata method."""

    def test_returns_content_and_metadata(
        self, tmp_path, sample_report_content, sample_metadata
    ):
        """Should return both content and metadata."""
        storage = FileReportStorage(base_dir=tmp_path)

        # Create files
        report_path = tmp_path / "test-uuid.md"
        report_path.write_text(sample_report_content, encoding="utf-8")

        metadata_path = tmp_path / "test-uuid_metadata.json"
        metadata_path.write_text(json.dumps(sample_metadata), encoding="utf-8")

        result = storage.get_report_with_metadata("test-uuid")

        assert result["content"] == sample_report_content
        assert result["metadata"] == sample_metadata

    def test_returns_empty_metadata_when_missing(
        self, tmp_path, sample_report_content
    ):
        """Should return empty metadata when file missing."""
        storage = FileReportStorage(base_dir=tmp_path)

        report_path = tmp_path / "test-uuid.md"
        report_path.write_text(sample_report_content, encoding="utf-8")

        result = storage.get_report_with_metadata("test-uuid")

        assert result["content"] == sample_report_content
        assert result["metadata"] == {}

    def test_returns_none_when_report_missing(self, tmp_path):
        """Should return None when report doesn't exist."""
        storage = FileReportStorage(base_dir=tmp_path)

        result = storage.get_report_with_metadata("nonexistent")

        assert result is None


class TestDeleteReport:
    """Tests for delete_report method."""

    def test_deletes_report_file(self, tmp_path, sample_report_content):
        """Should delete report file."""
        storage = FileReportStorage(base_dir=tmp_path)

        report_path = tmp_path / "test-uuid.md"
        report_path.write_text(sample_report_content, encoding="utf-8")

        result = storage.delete_report("test-uuid")

        assert result is True
        assert not report_path.exists()

    def test_deletes_metadata_file(
        self, tmp_path, sample_report_content, sample_metadata
    ):
        """Should delete metadata file."""
        storage = FileReportStorage(base_dir=tmp_path)

        report_path = tmp_path / "test-uuid.md"
        report_path.write_text(sample_report_content, encoding="utf-8")

        metadata_path = tmp_path / "test-uuid_metadata.json"
        metadata_path.write_text(json.dumps(sample_metadata), encoding="utf-8")

        storage.delete_report("test-uuid")

        assert not metadata_path.exists()

    def test_returns_false_when_files_not_exist(self, tmp_path):
        """Should return False when no files to delete."""
        storage = FileReportStorage(base_dir=tmp_path)

        result = storage.delete_report("nonexistent")

        assert result is False

    def test_returns_false_on_error(self, tmp_path, sample_report_content):
        """Should return False on delete error."""
        storage = FileReportStorage(base_dir=tmp_path)

        report_path = tmp_path / "test-uuid.md"
        report_path.write_text(sample_report_content, encoding="utf-8")

        with patch.object(
            Path, "unlink", side_effect=Exception("delete error")
        ):
            result = storage.delete_report("test-uuid")

        assert result is False


class TestReportExists:
    """Tests for report_exists method."""

    def test_returns_true_when_file_exists(
        self, tmp_path, sample_report_content
    ):
        """Should return True when file exists."""
        storage = FileReportStorage(base_dir=tmp_path)

        report_path = tmp_path / "test-uuid.md"
        report_path.write_text(sample_report_content, encoding="utf-8")

        result = storage.report_exists("test-uuid")

        assert result is True

    def test_returns_false_when_file_not_exists(self, tmp_path):
        """Should return False when file doesn't exist."""
        storage = FileReportStorage(base_dir=tmp_path)

        result = storage.report_exists("nonexistent")

        assert result is False


class TestListReports:
    """Tests for list_reports method."""

    def test_returns_list_of_report_dicts(self, tmp_path):
        """Should return list of dicts for each .md file."""
        (tmp_path / "report-1.md").write_text("# Report 1", encoding="utf-8")
        (tmp_path / "report-2.md").write_text("# Report 2", encoding="utf-8")

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports()

        assert len(result) == 2
        ids = {r["id"] for r in result}
        assert ids == {"report-1", "report-2"}
        for r in result:
            assert "id" in r
            assert "query" in r
            assert "mode" in r
            assert "created_at" in r
            assert "completed_at" in r

    def test_returns_empty_list_when_no_reports(self, tmp_path):
        """Should return empty list when directory has no .md files."""
        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports()

        assert result == []

    def test_loads_metadata_from_json(self, tmp_path):
        """Should populate query and mode from metadata JSON when available."""
        (tmp_path / "report-1.md").write_text("# Report", encoding="utf-8")
        metadata = {"query": "test query", "mode": "detailed"}
        (tmp_path / "report-1_metadata.json").write_text(
            json.dumps(metadata), encoding="utf-8"
        )

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports()

        assert len(result) == 1
        assert result[0]["query"] == "test query"
        assert result[0]["mode"] == "detailed"

    def test_handles_missing_metadata(self, tmp_path):
        """Should return None query and 'unknown' mode when no metadata file."""
        (tmp_path / "report-1.md").write_text("# Report", encoding="utf-8")

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports()

        assert len(result) == 1
        assert result[0]["query"] is None
        assert result[0]["mode"] == "unknown"

    def test_returns_empty_list_on_error(self, tmp_path):
        """Should return empty list on error."""
        storage = FileReportStorage(base_dir=tmp_path)

        with patch.object(Path, "glob", side_effect=Exception("glob error")):
            result = storage.list_reports()

        assert result == []

    def test_accepts_username_parameter(self, tmp_path):
        """Should accept username parameter without error."""
        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports(username="testuser")

        assert result == []

    def test_ignores_non_md_files(self, tmp_path):
        """Should only list .md files, ignoring other file types."""
        (tmp_path / "report-1.md").write_text("# Report", encoding="utf-8")
        (tmp_path / "report-1_metadata.json").write_text("{}", encoding="utf-8")
        (tmp_path / "notes.txt").write_text("notes", encoding="utf-8")
        (tmp_path / "data.csv").write_text("a,b", encoding="utf-8")

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports()

        assert len(result) == 1
        assert result[0]["id"] == "report-1"

    def test_created_at_is_iso_timestamp(self, tmp_path):
        """Should return created_at as ISO 8601 string, not raw float."""
        (tmp_path / "report-1.md").write_text("# Report", encoding="utf-8")

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports()

        created_at = result[0]["created_at"]
        assert isinstance(created_at, str)
        # ISO format should contain 'T' separator and timezone info
        assert "T" in created_at

    def test_handles_corrupt_metadata_json(self, tmp_path):
        """Should gracefully handle corrupt metadata JSON."""
        (tmp_path / "report-1.md").write_text("# Report", encoding="utf-8")
        (tmp_path / "report-1_metadata.json").write_text(
            "not valid json{{{", encoding="utf-8"
        )

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports()

        assert len(result) == 1
        assert result[0]["query"] is None
        assert result[0]["mode"] == "unknown"

    def test_metadata_with_extra_fields_only_uses_query_and_mode(
        self, tmp_path
    ):
        """Should extract only query and mode from metadata."""
        (tmp_path / "report-1.md").write_text("# Report", encoding="utf-8")
        metadata = {
            "query": "my query",
            "mode": "detailed",
            "extra_field": "ignored",
            "sources_count": 42,
        }
        (tmp_path / "report-1_metadata.json").write_text(
            json.dumps(metadata), encoding="utf-8"
        )

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage.list_reports()

        assert set(result[0].keys()) == {
            "id",
            "query",
            "mode",
            "created_at",
            "completed_at",
        }


class TestLoadMetadata:
    """Tests for _load_metadata helper method."""

    def test_returns_dict_when_metadata_exists(self, tmp_path):
        """Should return parsed dict from metadata JSON."""
        metadata = {"query": "test", "mode": "quick"}
        (tmp_path / "r1_metadata.json").write_text(
            json.dumps(metadata), encoding="utf-8"
        )

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage._load_metadata("r1")

        assert result == metadata

    def test_returns_none_when_no_metadata_file(self, tmp_path):
        """Should return None when metadata file doesn't exist."""
        storage = FileReportStorage(base_dir=tmp_path)
        result = storage._load_metadata("nonexistent")

        assert result is None

    def test_returns_none_on_invalid_json(self, tmp_path):
        """Should return None when metadata file has invalid JSON."""
        (tmp_path / "r1_metadata.json").write_text("{{bad", encoding="utf-8")

        storage = FileReportStorage(base_dir=tmp_path)
        result = storage._load_metadata("r1")

        assert result is None
