"""Unit tests for handler/model_sync.py"""

import time
import pytest
from unittest.mock import patch, MagicMock

_mock_env = MagicMock()
_mock_env.model_sync.model_from = ""
_mock_env.model_sync.model_path = "/data/models"
_mock_env.model_sync.sync_result_file = ".model_sync_status"
_mock_env.model_sync.wait_enabled = True
_mock_env.model_sync.wait_timeout = 5
_mock_env.model_sync.wait_interval = 0.1

with patch("gmi_ieops.handler.model_sync.env", _mock_env):
    from gmi_ieops.handler.model_sync import (
        SyncStatus, ModelSyncStatus, check_model_sync_status, wait_for_model_sync,
    )
    import gmi_ieops.handler.model_sync as _mod
_mod.env = _mock_env


class TestModelSyncStatus:

    @pytest.mark.parametrize("status,ready,failed", [
        (SyncStatus.DONE, True, False),
        (SyncStatus.NO_SYNC_NEEDED, True, False),
        (SyncStatus.FAILED, False, True),
        (SyncStatus.RUNNING, False, False),
        (SyncStatus.NOT_FOUND, False, False),
    ])
    def test_properties(self, status, ready, failed):
        s = ModelSyncStatus(status, "", "", "/data")
        assert s.is_ready == ready and s.is_failed == failed


class TestCheckModelSyncStatus:

    def test_no_model_from(self):
        _mock_env.model_sync.model_from = ""
        assert check_model_sync_status().status == SyncStatus.NO_SYNC_NEEDED

    def test_file_not_found(self, tmp_path):
        _mock_env.model_sync.model_from = "hf://model"
        assert check_model_sync_status(str(tmp_path), ".sync").status == SyncStatus.NOT_FOUND

    @pytest.mark.parametrize("content,expected", [
        ("done", SyncStatus.DONE),
        ("DONE\n", SyncStatus.DONE),
        ("running", SyncStatus.RUNNING),
        ("failed", SyncStatus.FAILED),
        ("unknown_value", SyncStatus.RUNNING),
    ])
    def test_status_from_file(self, tmp_path, content, expected):
        _mock_env.model_sync.model_from = "hf://model"
        (tmp_path / ".sync").write_text(content)
        assert check_model_sync_status(str(tmp_path), ".sync").status == expected

    def test_result_fields(self, tmp_path):
        _mock_env.model_sync.model_from = "gmifs://m"
        (tmp_path / ".s").write_text("done")
        s = check_model_sync_status(str(tmp_path), ".s")
        assert s.model_from == "gmifs://m" and s.model_path == str(tmp_path)


class TestWaitForModelSync:

    def _wait(self, tmp_path, status_text, **kw):
        _mock_env.model_sync.model_from = "hf://model"
        _mock_env.model_sync.wait_enabled = kw.pop("wait_enabled", True)
        (tmp_path / ".sync").write_text(status_text)
        return wait_for_model_sync(
            model_path=str(tmp_path), sync_result_file=".sync",
            timeout=kw.get("timeout", 5), interval=kw.get("interval", 0.1),
            logger=MagicMock(), **{k: v for k, v in kw.items() if k not in ("timeout", "interval", "wait_enabled")},
        )

    def test_immediate_done(self, tmp_path):
        assert self._wait(tmp_path, "done") is True

    def test_immediate_failed(self, tmp_path):
        assert self._wait(tmp_path, "failed") is False

    def test_timeout(self, tmp_path):
        start = time.time()
        assert self._wait(tmp_path, "running", timeout=0.3, interval=0.05) is False
        assert time.time() - start < 2

    def test_wait_disabled_ready(self, tmp_path):
        _mock_env.model_sync.model_from = ""
        _mock_env.model_sync.wait_enabled = False
        result = wait_for_model_sync(str(tmp_path), ".sync", logger=MagicMock())
        assert result is True

    def test_wait_disabled_not_ready_raises(self, tmp_path):
        (tmp_path / ".sync").write_text("running")
        with pytest.raises(RuntimeError, match="not ready"):
            self._wait(tmp_path, "running", wait_enabled=False)

    def test_callback(self, tmp_path):
        cb = MagicMock()
        self._wait(tmp_path, "done", on_status_change=cb)
        cb.assert_called_once()
        assert cb.call_args[0][0].status == SyncStatus.DONE

    def test_negative_interval_clamped(self, tmp_path):
        """Negative interval should be clamped to 1, not crash time.sleep."""
        assert self._wait(tmp_path, "done", interval=-5) is True

    def test_zero_interval_clamped(self, tmp_path):
        """Zero interval should be clamped to 1, not spin-loop."""
        assert self._wait(tmp_path, "done", interval=0) is True

    def test_negative_timeout_clamped(self, tmp_path):
        """Negative timeout should be clamped to 1, not immediately fail."""
        # With done status, it should return True on first check regardless of timeout
        assert self._wait(tmp_path, "done", timeout=-10) is True

    def test_zero_timeout_running_fails(self, tmp_path):
        """Zero timeout on running status should be clamped to 1 and eventually timeout."""
        start = time.time()
        assert self._wait(tmp_path, "running", timeout=0, interval=0) is False
        elapsed = time.time() - start
        # timeout=0 clamped to 1, interval=0 clamped to 1 → takes ~1s
        assert elapsed >= 0.5
