import pytest
from conftest import BUCKET_NAME, PREFIX, STS_CREDS

from vcm_file_uploader._types import UploadFileEntry
from vcm_file_uploader.plan import UploadPlan
from vcm_file_uploader.session import PrefixError, STSUploadSession


def _make_session(**overrides):
    kwargs = {**STS_CREDS, "bucket": BUCKET_NAME, "prefix": PREFIX}
    kwargs.update(overrides)
    return STSUploadSession(**kwargs)


def _write_file(tmp_path, name, content=b"hello world"):
    p = tmp_path / name
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_bytes(content)
    return str(p)


class TestUploadFile:
    def test_upload_single_file(self, mock_s3, tmp_path):
        bucket_name, client = mock_s3
        local = _write_file(tmp_path, "data.txt", b"test content")
        key = PREFIX + "data.txt"

        with _make_session() as session:
            result = session.upload_file(local, key)

        assert result.success is True
        assert result.s3_key == key
        assert result.size == len(b"test content")
        assert result.error is None

        # Verify object exists in S3
        obj = client.get_object(Bucket=bucket_name, Key=key)
        assert obj["Body"].read() == b"test content"

    def test_upload_with_extra_args(self, mock_s3, tmp_path):
        bucket_name, client = mock_s3
        local = _write_file(tmp_path, "data.txt")
        key = PREFIX + "data.txt"

        with _make_session() as session:
            result = session.upload_file(
                local, key, extra_args={"ContentType": "text/plain"}
            )

        assert result.success is True
        obj = client.head_object(Bucket=bucket_name, Key=key)
        assert obj["ContentType"] == "text/plain"

    def test_upload_missing_file_returns_failure(self, mock_s3):
        key = PREFIX + "missing.txt"
        with _make_session() as session:
            result = session.upload_file("/nonexistent/file.txt", key)

        assert result.success is False
        assert result.error is not None


class TestPrefixEnforcement:
    def test_rejects_key_outside_prefix(self, mock_s3, tmp_path):
        local = _write_file(tmp_path, "data.txt")

        with _make_session() as session:
            with pytest.raises(PrefixError, match="outside allowed prefix"):
                session.upload_file(local, "other-prefix/data.txt")

    def test_accepts_key_within_prefix(self, mock_s3, tmp_path):
        local = _write_file(tmp_path, "data.txt")
        key = PREFIX + "subdir/data.txt"

        with _make_session() as session:
            result = session.upload_file(local, key)

        assert result.success is True

    def test_prefix_normalized_with_trailing_slash(self, mock_s3, tmp_path):
        """Prefix without trailing slash should still work."""
        local = _write_file(tmp_path, "data.txt")
        session = _make_session(prefix=PREFIX.rstrip("/"))
        key = PREFIX + "data.txt"

        result = session.upload_file(local, key)
        assert result.success is True
        session.close()


class TestUploadFiles:
    def test_uploads_full_plan(self, mock_s3, tmp_path):
        bucket_name, client = mock_s3
        # Create local files
        files = []
        for name, size in [("big.nc", 1000), ("med.txt", 500), ("small.log", 100)]:
            path = _write_file(tmp_path, name, b"x" * size)
            files.append(
                UploadFileEntry(path=name, full_path=path, size=size)
            )

        plan = UploadPlan(files=files)

        with _make_session() as session:
            summary = session.upload_files(plan)

        assert summary.all_succeeded is True
        assert len(summary.succeeded) == 3
        assert summary.total_bytes == 1600

        # Verify all objects exist
        response = client.list_objects_v2(Bucket=bucket_name, Prefix=PREFIX)
        keys = {obj["Key"] for obj in response["Contents"]}
        assert PREFIX + "big.nc" in keys
        assert PREFIX + "med.txt" in keys
        assert PREFIX + "small.log" in keys

    def test_upload_order_is_largest_first(self, mock_s3, tmp_path):
        """Plan iterates largest first; verify results come back in that order."""
        files = []
        for name, size in [("small.txt", 10), ("big.txt", 1000), ("med.txt", 500)]:
            path = _write_file(tmp_path, name, b"x" * size)
            files.append(
                UploadFileEntry(path=name, full_path=path, size=size)
            )

        plan = UploadPlan(files=files)

        with _make_session() as session:
            summary = session.upload_files(plan)

        result_sizes = [r.size for r in summary.results]
        assert result_sizes == [1000, 500, 10]

    def test_empty_plan(self, mock_s3):
        plan = UploadPlan(files=[])

        with _make_session() as session:
            summary = session.upload_files(plan)

        assert len(summary.results) == 0
        assert summary.all_succeeded is False  # no results = not all_succeeded


class TestPartialFailure:
    def test_continues_after_failure(self, mock_s3, tmp_path):
        """If one file is missing, the rest should still upload."""
        files = [
            UploadFileEntry(path="good.txt", full_path=_write_file(tmp_path, "good.txt"), size=11),
            UploadFileEntry(path="missing.txt", full_path="/nonexistent/missing.txt", size=0),
            UploadFileEntry(
                path="also_good.txt",
                full_path=_write_file(tmp_path, "also_good.txt"),
                size=11,
            ),
        ]
        plan = UploadPlan(files=files)

        with _make_session() as session:
            summary = session.upload_files(plan)

        assert len(summary.succeeded) == 2
        assert len(summary.failed) == 1
        assert summary.all_succeeded is False
        assert summary.failed[0].error is not None

    def test_exit_code_success(self, mock_s3, tmp_path):
        files = [
            UploadFileEntry(path="a.txt", full_path=_write_file(tmp_path, "a.txt"), size=11),
        ]
        plan = UploadPlan(files=files)

        with _make_session() as session:
            summary = session.upload_files(plan)

        assert summary.exit_code == 0

    def test_exit_code_partial(self, mock_s3, tmp_path):
        files = [
            UploadFileEntry(path="good.txt", full_path=_write_file(tmp_path, "good.txt"), size=11),
            UploadFileEntry(path="bad.txt", full_path="/nonexistent/bad.txt", size=0),
        ]
        plan = UploadPlan(files=files)

        with _make_session() as session:
            summary = session.upload_files(plan)

        assert summary.exit_code == 1

    def test_exit_code_total_failure(self, mock_s3):
        files = [
            UploadFileEntry(path="bad1.txt", full_path="/nonexistent/a.txt", size=0),
            UploadFileEntry(path="bad2.txt", full_path="/nonexistent/b.txt", size=0),
        ]
        plan = UploadPlan(files=files)

        with _make_session() as session:
            summary = session.upload_files(plan)

        assert summary.exit_code == 2

    def test_exit_code_empty(self, mock_s3):
        plan = UploadPlan(files=[])

        with _make_session() as session:
            summary = session.upload_files(plan)

        assert summary.exit_code == 2


class TestRetryConfig:
    def test_custom_retry_params(self, mock_s3, tmp_path):
        """Session should accept retry configuration without error."""
        local = _write_file(tmp_path, "data.txt")
        key = PREFIX + "data.txt"

        session = _make_session(max_retries=10, retry_mode="standard")
        result = session.upload_file(local, key)
        assert result.success is True
        session.close()

    def test_default_retry_params(self, mock_s3, tmp_path):
        """Default retry config should work."""
        local = _write_file(tmp_path, "data.txt")
        key = PREFIX + "data.txt"

        with _make_session() as session:
            result = session.upload_file(local, key)
        assert result.success is True


class TestContextManager:
    def test_works_as_context_manager(self, mock_s3, tmp_path):
        local = _write_file(tmp_path, "data.txt")
        key = PREFIX + "data.txt"

        with _make_session() as session:
            result = session.upload_file(local, key)
            assert result.success is True
        assert session._closed is True

    def test_close_is_idempotent(self, mock_s3):
        session = _make_session()
        session.close()
        session.close()  # Should not raise
        assert session._closed is True
