"""Archive failure remains observable before the consumer's 30-minute validation job expires."""
import contextlib
import io
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch

import flutter_manifest


class DownloadBudgetTests(unittest.TestCase):
    def test_failed_archive_reports_before_the_outer_job_cancels_it(self):
        elapsed = [0.0]

        def transfer(_url, destination, _kind, remaining, _env):
            elapsed[0] += min(600, remaining)
            with destination.open("ab") as output:
                output.write(b"partial")
            return 28, 200, 200

        with tempfile.TemporaryDirectory() as directory, contextlib.redirect_stderr(io.StringIO()) as error:
            with patch.dict(os.environ, HTTPS_PROXY="http://fixture-user:fixture-secret@localhost:1",
                            GOWALK_FLUTTER_ARCHIVE_SHA256="a" * 64):
                with patch.object(flutter_manifest.time, "monotonic", side_effect=lambda: elapsed[0]), \
                        patch.object(flutter_manifest.time, "sleep", side_effect=lambda seconds: None), \
                        patch.object(flutter_manifest.flutter_download, "transfer", side_effect=transfer):
                    result = flutter_manifest.download(["--connect-timeout", "15", "--retry", "5",
                        "https://fixture.invalid/flutter_infra_release/releases/stable/linux/sdk.tar.xz"],
                        Path(directory))
        self.assertEqual(result, 28)
        self.assertLess(elapsed[0], 1800 - 60, "A failure receipt must precede the enclosing job deadline")
        receipt = next(line.split("::", 2)[2] for line in error.getvalue().splitlines()
                       if line.startswith("::error title=flutter_download_failed::"))
        self.assertGreater(json.loads(receipt)["retained_bytes"], 0)
        self.assertNotIn("fixture-secret", error.getvalue())


class ArchiveBudgetTests(unittest.TestCase):
    def test_a_release_job_may_declare_more_archive_room_than_a_validation_job(self):
        with patch.dict(os.environ, {}, clear=False):
            os.environ.pop("GOWALK_FLUTTER_ARCHIVE_BUDGET_S", None)
            self.assertEqual(flutter_manifest.archive_budget(), 1200)
            os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "2100"
            self.assertEqual(flutter_manifest.archive_budget(), 2100)
            # Never shorter than the default, never past a release job's timeout,
            # and a malformed value is the default rather than a refusal.
            os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "60"
            self.assertEqual(flutter_manifest.archive_budget(), 1200)
            os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "999999"
            self.assertEqual(flutter_manifest.archive_budget(), 3000)
            os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = "not-a-number"
            self.assertEqual(flutter_manifest.archive_budget(), 1200)
            os.environ["GOWALK_FLUTTER_ARCHIVE_BUDGET_S"] = ""
            self.assertEqual(flutter_manifest.archive_budget(), 1200)

    def test_a_declared_budget_actually_extends_the_transfer_deadline(self):
        elapsed = [0.0]

        def transfer(_url, destination, _kind, remaining, _env):
            elapsed[0] += min(600, remaining)
            with destination.open("ab") as output:
                output.write(b"partial")
            return 28, 200, 200

        with tempfile.TemporaryDirectory() as directory, contextlib.redirect_stderr(io.StringIO()):
            with patch.dict(os.environ, HTTPS_PROXY="http://fixture:secret@localhost:1",
                            GOWALK_FLUTTER_ARCHIVE_SHA256="a" * 64,
                            GOWALK_FLUTTER_ARCHIVE_BUDGET_S="2100"):
                with patch.object(flutter_manifest.time, "monotonic", side_effect=lambda: elapsed[0]), \
                        patch.object(flutter_manifest.time, "sleep", side_effect=lambda seconds: None), \
                        patch.object(flutter_manifest.flutter_download, "transfer", side_effect=transfer):
                    flutter_manifest.download(["--connect-timeout", "15", "--retry", "5",
                        "https://fixture.invalid/flutter_infra_release/releases/stable/linux/sdk.tar.xz"],
                        Path(directory))
        # The declared deadline bounds the pass, not the two 600 s attempts a
        # 1200 s default allowed: the last attempt gets the remaining 300 s.
        self.assertEqual(elapsed[0], 2100)




class ManifestRetryTests(unittest.TestCase):
    def test_a_briefly_refusing_exit_is_retried_until_the_deadline(self):
        """The iOS release lost its manifest to four TLS refusals in ten seconds
        (curl 35, proxy_status 200) with fifty seconds of budget unspent, an hour
        after the same fetch succeeded."""
        elapsed, attempts = [0.0], []

        def transfer(_url, destination, _kind, _remaining, _env):
            attempts.append(elapsed[0])
            elapsed[0] += 2.5          # a TLS refusal fails fast
            destination.write_bytes(b"")
            return 35, 0, 200

        with tempfile.TemporaryDirectory() as directory, contextlib.redirect_stderr(io.StringIO()) as error:
            with patch.dict(os.environ, HTTPS_PROXY="http://fixture:secret@localhost:1"):
                with patch.object(flutter_manifest.time, "monotonic", side_effect=lambda: elapsed[0]), \
                        patch.object(flutter_manifest.time, "sleep",
                                     side_effect=lambda seconds: elapsed.__setitem__(0, elapsed[0] + seconds)), \
                        patch.object(flutter_manifest.flutter_download, "transfer", side_effect=transfer):
                    result = flutter_manifest.download(["--silent", "--connect-timeout", "15", "--retry", "5",
                        "https://fixture.invalid/flutter_infra_release/releases/releases_macos.json"],
                        Path(directory))
        self.assertEqual(result, 35)
        # It used the manifest's whole 60 s window instead of giving up at 10 s.
        self.assertGreater(len(attempts), 4)
        self.assertLessEqual(len(attempts), flutter_manifest.MAX_ATTEMPTS["manifest"])
        self.assertGreater(elapsed[0], 30)
        # The deadline bounds the pass; the last attempt may end just past it.
        self.assertLess(elapsed[0], 70)
        self.assertIn("attempts=%d" % len(attempts), error.getvalue())
        self.assertNotIn("secret", error.getvalue())

    def test_an_exit_that_recovers_mid_window_still_publishes(self):
        elapsed, calls = [0.0], []

        def transfer(_url, destination, _kind, _remaining, _env):
            calls.append(1)
            elapsed[0] += 2.5
            if len(calls) < 6:
                destination.write_bytes(b"")
                return 35, 0, 200
            destination.write_bytes(b'{"releases": []}')
            return 0, 200, 200

        with tempfile.TemporaryDirectory() as directory, contextlib.redirect_stderr(io.StringIO()) as error:
            with patch.dict(os.environ, HTTPS_PROXY="http://fixture:secret@localhost:1"):
                with patch.object(flutter_manifest.time, "monotonic", side_effect=lambda: elapsed[0]), \
                        patch.object(flutter_manifest.time, "sleep",
                                     side_effect=lambda seconds: elapsed.__setitem__(0, elapsed[0] + seconds)), \
                        patch.object(flutter_manifest.flutter_download, "publish", side_effect=lambda *a: None), \
                        patch.object(flutter_manifest.flutter_download, "transfer", side_effect=transfer):
                    result = flutter_manifest.download(["--silent", "--connect-timeout", "15", "--retry", "5",
                        "https://fixture.invalid/flutter_infra_release/releases/releases_macos.json"],
                        Path(directory))
        self.assertEqual(result, 0)
        self.assertEqual(len(calls), 6)
        self.assertIn("flutter_manifest_recovered attempts=6", error.getvalue())


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