"""A single early edit verifies readiness and inventories AAB/APK version codes."""
import io
from pathlib import Path
import sys
import unittest
from unittest import mock

import requests

sys.path.insert(0, str(Path(__file__).resolve().parent))
import play_inventory as inventory
import play_preflight as preflight
import play_preflight_transport as transport


class PlayInventoryTests(unittest.TestCase):
    def setUp(self):
        self.client = mock.MagicMock()
        self.client.__enter__.return_value = self.client
        self.client.post.return_value = mock.Mock(status_code=200, text='{"id":"edit-123"}')
        self.client.delete.return_value = mock.Mock(status_code=204)
        self.client.get.side_effect = [mock.Mock(status_code=200, json=lambda: {"bundles": [{"versionCode": 41}]}),
                                       mock.Mock(status_code=200, json=lambda: {"apks": [{"versionCode": 55}]})]
        self.credentials = mock.Mock(token="private-token")
        self.enterContext(mock.patch.object(preflight.service_account.Credentials,
                                           "from_service_account_file", return_value=self.credentials))
        self.enterContext(mock.patch.object(inventory.requests, "Session", return_value=self.client))
        self.sleep = self.enterContext(mock.patch.object(transport.time, "sleep"))
        self.output = self.enterContext(mock.patch("sys.stdout", new_callable=io.StringIO))

    def read(self):
        return inventory.read_inventory("com.example.app", Path("account.json"))

    def test_bundles_and_apks_share_one_edit_and_highest_code(self):
        result = self.read()
        self.assertTrue(result.ready)
        self.assertEqual(result.highest_version, 55)
        self.credentials.refresh.assert_called_once()
        self.client.post.assert_called_once()
        self.client.delete.assert_called_once()
        self.assertEqual([c.args[0].rsplit("/", 1)[-1] for c in self.client.get.call_args_list], ["bundles", "apks"])

    def test_positive_package_absence_skips_inventory_and_allows_first_artifact(self):
        self.client.post.return_value.status_code = 404
        self.assertEqual(self.read(), inventory.PlayInventory(False, None))
        self.assertIn("assigned Play Console", self.output.getvalue())
        self.client.get.assert_not_called()
        self.client.delete.assert_not_called()

    def test_successful_empty_inventory_is_distinct_from_package_absence(self):
        self.client.get.side_effect = [mock.Mock(status_code=200, json=lambda: {}),
                                       mock.Mock(status_code=200, json=lambda: {})]
        self.assertEqual(self.read(), inventory.PlayInventory(True, None))
        self.client.delete.assert_called_once()

    def test_inventory_get_retry_keeps_the_same_edit(self):
        good = mock.Mock(status_code=200, json=lambda: {})
        self.client.get.side_effect = [requests.exceptions.ReadTimeout(), good, good]
        self.assertTrue(self.read().ready)
        self.assertEqual(self.client.get.call_count, 3)
        self.client.post.assert_called_once()
        self.client.delete.assert_called_once()

    def test_permission_denial_is_fatal_and_closes_the_owned_edit(self):
        self.client.get.side_effect = [mock.Mock(status_code=403, text="private-provider-body")]
        with self.assertRaises(SystemExit) as caught:
            self.read()
        self.assertIn('"phase": "version_inventory"', str(caught.exception))
        self.assertIn('"status": 403', str(caught.exception))
        self.assertNotIn("private", str(caught.exception) + self.output.getvalue())
        self.client.delete.assert_called_once()

    def test_lost_edit_is_not_misclassified_as_absent_package(self):
        self.client.get.side_effect = [mock.Mock(status_code=404)]
        with self.assertRaises(SystemExit):
            self.read()
        self.client.post.assert_called_once()
        self.client.delete.assert_called_once()

    def test_invalid_inventory_is_fatal_and_cleanup_still_runs(self):
        self.client.get.side_effect = [mock.Mock(status_code=200, json=lambda: {"bundles": [{"versionCode": "bad"}]})]
        with self.assertRaises(SystemExit) as caught:
            self.read()
        self.assertIn('"code": "invalid_response"', str(caught.exception))
        self.client.delete.assert_called_once()

    def test_cleanup_failure_prevents_success_and_retains_its_edit_identifier(self):
        self.client.delete.side_effect = requests.exceptions.ConnectionError("private-proxy")
        with self.assertRaises(SystemExit) as caught:
            self.read()
        self.assertIn('"cleanup_required": true', str(caught.exception))
        self.assertIn('"edit_id": "edit-123"', str(caught.exception))
        self.client.post.assert_called_once()
        self.assertEqual(self.client.delete.call_count, 3)


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