"""Private child transfers expose byte progress and terminate within their own deadline."""
import contextlib
import io
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
import unittest
from unittest.mock import patch

import flutter_download


class TransferProgressTests(unittest.TestCase):
    def invoke(self, script, remaining):
        children = []
        popen = subprocess.Popen
        with tempfile.TemporaryDirectory() as directory, contextlib.redirect_stderr(io.StringIO()) as error:
            destination = Path(directory) / "archive"
            destination.touch()

            def child(_command, **kwargs):
                process = popen([sys.executable, "-c", script, str(destination)], **kwargs)
                children.append(process)
                return process

            with patch.object(flutter_download.subprocess, "Popen", side_effect=child), \
                    patch.object(flutter_download, "PROGRESS_SECONDS", .03):
                result = flutter_download.transfer("https://fixture.invalid/sdk.tar.xz", destination,
                                                   "archive", remaining, dict(os.environ))
            data = destination.read_bytes()
        self.assertTrue(all(process.poll() is not None for process in children))
        self.assertNotIn("fixture-secret", error.getvalue())
        notices = [json.loads(line.split("::", 2)[2]) for line in error.getvalue().splitlines()]
        self.assertTrue(notices)
        self.assertTrue(any(note["retained_bytes"] > 0 for note in notices))
        self.assertTrue(all(note["event"] == "transferring" for note in notices))
        return result, data

    def test_progress_does_not_publish_private_child_stderr_or_partial_archive(self):
        script = ("import pathlib,sys,time; p=pathlib.Path(sys.argv[1]); p.write_bytes(b'partial'); "
                  "sys.stderr.write('fixture-secret'); time.sleep(.2); p.write_bytes(b'complete'); "
                  "print('206 200',end='')")
        result, data = self.invoke(script, 2)
        self.assertEqual((result, data), ((0, 206, 200), b"complete"))

    def test_deadline_kills_only_the_owned_transfer_and_preserves_resume_bytes(self):
        script = ("import pathlib,sys,time; pathlib.Path(sys.argv[1]).write_bytes(b'partial'); "
                  "sys.stderr.write('fixture-secret'); time.sleep(5)")
        started = time.monotonic()
        result, data = self.invoke(script, .2)
        self.assertLess(time.monotonic() - started, 2)
        self.assertEqual((result, data), ((28, 0, 0), b"partial"))

    def test_progress_failure_does_not_leave_the_owned_child_running(self):
        popen, children = subprocess.Popen, []

        def child(_command, **kwargs):
            process = popen([sys.executable, "-c", "import time; time.sleep(5)"], **kwargs)
            children.append(process)
            return process

        started = time.monotonic()
        with tempfile.TemporaryDirectory() as directory:
            destination = Path(directory) / "archive"
            destination.touch()
            with patch.object(flutter_download.subprocess, "Popen", side_effect=child), \
                    patch.object(flutter_download, "PROGRESS_SECONDS", .03), \
                    patch.object(flutter_download, "progress", side_effect=OSError("closed output")):
                with self.assertRaises(OSError):
                    flutter_download.transfer("https://fixture.invalid/sdk.tar.xz", destination,
                                              "archive", 2, dict(os.environ))
        self.assertLess(time.monotonic() - started, 2)
        self.assertTrue(all(process.poll() is not None for process in children))


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