import json

import pytest

from vcm_file_uploader._types import UploadFileEntry
from vcm_file_uploader.plan import UploadPlan, UploadPlanError

SAMPLE_PLAN = {
    "timestamp": "20260213T120000",
    "files": [
        {
            "path": "t01/amber.nc",
            "full_path": "/work/t01/amber.nc",
            "size": 100,
            "category": "trajectory",
            "sha256": "abc123",
            "reason": "new",
            "multipart": False,
        },
        {
            "path": "t01/amber.mdout",
            "full_path": "/work/t01/amber.mdout",
            "size": 500_000_000,
            "category": "log",
            "sha256": None,
            "reason": "modified",
            "multipart": True,
        },
        {
            "path": "t01/restart.rst7",
            "full_path": "/work/t01/restart.rst7",
            "size": 50_000,
            "category": "restart",
            "sha256": "def456",
            "reason": "new",
            "multipart": False,
        },
    ],
    "deletions": ["old_file.txt"],
    "summary": {
        "upload_count": 3,
        "upload_bytes": 500_050_100,
        "deletion_count": 1,
        "multipart_count": 1,
    },
}


class TestUploadPlanFromDict:
    def test_loads_files(self):
        plan = UploadPlan.from_dict(SAMPLE_PLAN)
        assert len(plan) == 3

    def test_sorted_largest_first(self):
        plan = UploadPlan.from_dict(SAMPLE_PLAN)
        sizes = [f.size for f in plan]
        assert sizes == sorted(sizes, reverse=True)

    def test_file_entry_fields(self):
        plan = UploadPlan.from_dict(SAMPLE_PLAN)
        # Largest file should be first
        first = plan.files[0]
        assert first.path == "t01/amber.mdout"
        assert first.size == 500_000_000
        assert first.category == "log"
        assert first.multipart is True
        assert first.reason == "modified"

    def test_deletions(self):
        plan = UploadPlan.from_dict(SAMPLE_PLAN)
        assert plan.deletions == ["old_file.txt"]

    def test_timestamp(self):
        plan = UploadPlan.from_dict(SAMPLE_PLAN)
        assert plan.timestamp == "20260213T120000"

    def test_total_bytes(self):
        plan = UploadPlan.from_dict(SAMPLE_PLAN)
        assert plan.total_bytes == 500_050_100

    def test_multipart_count(self):
        plan = UploadPlan.from_dict(SAMPLE_PLAN)
        assert plan.multipart_count == 1

    def test_iterable(self):
        plan = UploadPlan.from_dict(SAMPLE_PLAN)
        entries = list(plan)
        assert len(entries) == 3
        assert all(isinstance(e, UploadFileEntry) for e in entries)


class TestUploadPlanFromJson:
    def test_loads_from_file(self, tmp_path):
        plan_file = tmp_path / "upload_20260213T120000.json"
        plan_file.write_text(json.dumps(SAMPLE_PLAN))
        plan = UploadPlan.from_json(plan_file)
        assert len(plan) == 3
        assert plan.files[0].size >= plan.files[1].size

    def test_missing_file(self, tmp_path):
        with pytest.raises(UploadPlanError, match="Failed to read"):
            UploadPlan.from_json(tmp_path / "nonexistent.json")

    def test_invalid_json(self, tmp_path):
        bad_file = tmp_path / "bad.json"
        bad_file.write_text("not json {{{")
        with pytest.raises(UploadPlanError, match="Failed to read"):
            UploadPlan.from_json(bad_file)


class TestUploadPlanValidation:
    def test_not_a_dict(self):
        with pytest.raises(UploadPlanError, match="must be a JSON object"):
            UploadPlan.from_dict("not a dict")  # type: ignore[arg-type]

    def test_missing_files_key(self):
        with pytest.raises(UploadPlanError, match="missing required key"):
            UploadPlan.from_dict({"deletions": []})

    def test_files_not_a_list(self):
        with pytest.raises(UploadPlanError, match="must be a list"):
            UploadPlan.from_dict({"files": "nope"})

    def test_file_entry_not_a_dict(self):
        with pytest.raises(UploadPlanError, match="expected object"):
            UploadPlan.from_dict({"files": ["not a dict"]})

    def test_file_entry_missing_fields(self):
        with pytest.raises(UploadPlanError, match="missing required fields"):
            UploadPlan.from_dict({"files": [{"path": "x"}]})

    def test_negative_size(self):
        with pytest.raises(UploadPlanError, match="non-negative"):
            UploadPlan.from_dict({
                "files": [{"path": "x", "full_path": "/x", "size": -1}]
            })

    def test_size_not_a_number(self):
        with pytest.raises(UploadPlanError, match="non-negative"):
            UploadPlan.from_dict({
                "files": [{"path": "x", "full_path": "/x", "size": "big"}]
            })


class TestUploadPlanMinimalInput:
    def test_minimal_file_entry(self):
        """Only required fields provided — defaults should apply."""
        plan = UploadPlan.from_dict({
            "files": [{"path": "a.txt", "full_path": "/tmp/a.txt", "size": 42}]
        })
        assert len(plan) == 1
        entry = plan.files[0]
        assert entry.path == "a.txt"
        assert entry.size == 42
        assert entry.category is None
        assert entry.sha256 is None
        assert entry.reason == "unknown"
        assert entry.multipart is False

    def test_empty_files_list(self):
        plan = UploadPlan.from_dict({"files": []})
        assert len(plan) == 0
        assert plan.total_bytes == 0

    def test_constructor_sorts(self):
        entries = [
            UploadFileEntry(path="small", full_path="/small", size=10),
            UploadFileEntry(path="big", full_path="/big", size=1000),
        ]
        plan = UploadPlan(files=entries)
        assert plan.files[0].path == "big"
        assert plan.files[1].path == "small"
