#!/usr/bin/env python3
"""Tests for deploy_playstore.py."""
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, call, patch

sys.path.insert(0, str(Path(__file__).parent))

import deploy_playstore


def _make_session(
    edit_id="edit-42",
    version_code=7,
    upload_side_effects=None,
):
    """Return a mock AuthorizedSession with sensible defaults."""
    session = MagicMock()

    # POST /edits → create edit
    edit_resp = MagicMock()
    edit_resp.json.return_value = {"id": edit_id}
    session.post.return_value = edit_resp

    # POST resumable-init → Location header
    init_resp = MagicMock()
    init_resp.headers = {"Location": "https://upload.example.com/session"}

    # PUT upload → bundle JSON
    upload_resp = MagicMock()
    upload_resp.json.return_value = {"versionCode": version_code}

    if upload_side_effects is not None:
        # Use side_effect list: first call is edit create, rest are upload inits
        # We override the PUT side effects via _upload_aab_resumable mock instead
        pass

    return session, init_resp, upload_resp


class TestMainEnvChecks(unittest.TestCase):
    def test_missing_env_exits(self):
        with patch.dict(os.environ, {}, clear=True):
            with self.assertRaises(SystemExit) as ctx:
                deploy_playstore.main()
        self.assertEqual(ctx.exception.code, 1)

    def test_missing_aab_exits(self):
        fake_config = '{"type": "service_account"}'
        with patch.dict(os.environ, {"PLAY_STORE_CONFIG_JSON": fake_config}):
            with patch("deploy_playstore.os.path.exists", return_value=False):
                with self.assertRaises(SystemExit) as ctx:
                    deploy_playstore.main()
        self.assertEqual(ctx.exception.code, 1)


class TestMainHappyPath(unittest.TestCase):
    def _run_main(self, fake_config='{"type":"service_account"}'):
        mock_session = MagicMock()
        # POST for edit create and commit
        post_responses = [
            MagicMock(**{"json.return_value": {"id": "edit-42"}}),  # create edit
            MagicMock(),  # commit
        ]
        mock_session.post.side_effect = post_responses
        # PUT for track update
        mock_session.put.return_value = MagicMock()

        with patch.dict(os.environ, {"PLAY_STORE_CONFIG_JSON": fake_config}):
            with patch("deploy_playstore.os.path.exists", return_value=True):
                with patch("deploy_playstore.service_account.Credentials.from_service_account_info"):
                    with patch("deploy_playstore.AuthorizedSession", return_value=mock_session):
                        with patch(
                            "deploy_playstore._upload_aab_resumable",
                            return_value={"versionCode": 7},
                        ):
                            deploy_playstore.main()

        return mock_session

    def test_creates_edit(self):
        session = self._run_main()
        create_call = session.post.call_args_list[0]
        self.assertIn("/edits", create_call[0][0])

    def test_commits_edit(self):
        session = self._run_main()
        commit_call = session.post.call_args_list[1]
        self.assertIn(":commit", commit_call[0][0])

    def test_updates_track(self):
        session = self._run_main()
        track_call = session.put.call_args_list[0]
        self.assertIn("/tracks/", track_call[0][0])


class TestUploadRetry(unittest.TestCase):
    def _run_main(self, upload_side_effects, sleep_mock=None):
        mock_session = MagicMock()
        post_responses = [
            MagicMock(**{"json.return_value": {"id": "edit-1"}}),
            MagicMock(),
        ]
        mock_session.post.side_effect = post_responses
        mock_session.put.return_value = MagicMock()

        patches = [
            patch.dict(os.environ, {"PLAY_STORE_CONFIG_JSON": '{"type":"service_account"}'}),
            patch("deploy_playstore.os.path.exists", return_value=True),
            patch("deploy_playstore.service_account.Credentials.from_service_account_info"),
            patch("deploy_playstore.AuthorizedSession", return_value=mock_session),
            patch("deploy_playstore._upload_aab_resumable", side_effect=upload_side_effects),
            patch("deploy_playstore.time.sleep"),
        ]
        for p in patches:
            p.start()
        try:
            deploy_playstore.main()
        finally:
            for p in patches:
                p.stop()

    def test_succeeds_on_first_attempt(self):
        with patch("deploy_playstore._upload_aab_resumable", return_value={"versionCode": 5}) as mock_upload:
            with patch.dict(os.environ, {"PLAY_STORE_CONFIG_JSON": '{"type":"service_account"}'}):
                with patch("deploy_playstore.os.path.exists", return_value=True):
                    with patch("deploy_playstore.service_account.Credentials.from_service_account_info"):
                        mock_session = MagicMock()
                        mock_session.post.side_effect = [
                            MagicMock(**{"json.return_value": {"id": "e1"}}),
                            MagicMock(),
                        ]
                        mock_session.put.return_value = MagicMock()
                        with patch("deploy_playstore.AuthorizedSession", return_value=mock_session):
                            deploy_playstore.main()
            mock_upload.assert_called_once()

    def test_retries_once_on_error_then_succeeds(self):
        self._run_main([ValueError("transient"), {"versionCode": 9}])

    def test_raises_after_all_attempts_exhausted(self):
        with self.assertRaises(RuntimeError) as ctx:
            self._run_main([ValueError("err"), ValueError("err"), ValueError("err")])
        self.assertIn(str(deploy_playstore._MAX_UPLOAD_ATTEMPTS), str(ctx.exception))

    def test_backoff_delays_are_10s_then_20s(self):
        mock_session = MagicMock()
        mock_session.post.side_effect = [
            MagicMock(**{"json.return_value": {"id": "e1"}}),
            MagicMock(),
        ]
        mock_session.put.return_value = MagicMock()

        with patch.dict(os.environ, {"PLAY_STORE_CONFIG_JSON": '{"type":"service_account"}'}):
            with patch("deploy_playstore.os.path.exists", return_value=True):
                with patch("deploy_playstore.service_account.Credentials.from_service_account_info"):
                    with patch("deploy_playstore.AuthorizedSession", return_value=mock_session):
                        with patch(
                            "deploy_playstore._upload_aab_resumable",
                            side_effect=[ValueError("e"), ValueError("e"), {"versionCode": 3}],
                        ):
                            with patch("deploy_playstore.time.sleep") as mock_sleep:
                                deploy_playstore.main()

        mock_sleep.assert_has_calls([call(10), call(20)])


class TestUploadAabResumable(unittest.TestCase):
    def test_initiates_and_uploads(self):
        mock_session = MagicMock()
        init_resp = MagicMock()
        init_resp.headers = {"Location": "https://upload.example.com/sess"}
        upload_resp = MagicMock()
        upload_resp.json.return_value = {"versionCode": 42}
        mock_session.post.return_value = init_resp
        mock_session.put.return_value = upload_resp

        import tempfile
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(b"fake-aab-content")
            aab_path = f.name

        try:
            result = deploy_playstore._upload_aab_resumable(
                mock_session, "com.example.app", "edit-1", aab_path
            )
        finally:
            os.unlink(aab_path)

        self.assertEqual(result["versionCode"], 42)
        mock_session.post.assert_called_once()
        mock_session.put.assert_called_once()
        put_call = mock_session.put.call_args
        self.assertEqual(put_call[0][0], "https://upload.example.com/sess")


if __name__ == "__main__":
    unittest.main()
