import json
from pathlib import Path
import subprocess
import tempfile
import unittest

import configure_vhost
import edge_vhost
from test_edge_inputs import configured


class Vhosts(unittest.TestCase):
    def setUp(self):
        self.directory = tempfile.TemporaryDirectory()
        self.addCleanup(self.directory.cleanup)
        self.root = Path(self.directory.name)
        self.config = self.root / ".backend-edge.json"
        self.config.write_text(json.dumps(configured()))
        self.legacy, self.edge, self.include = edge_vhost.paths(self.config, "fixture", self.root)
        self.calls = []

    def run_nginx(self, command, **kwargs):
        self.calls.append(command)

    def test_legacy_tls_port_changes_and_replay(self):
        original = configure_vhost.content("", "legacy.example", 31000).replace(
            "listen 80;", "listen 443 ssl;\n    ssl_certificate /existing/cert;")
        self.legacy.write_text(original)
        self.legacy.chmod(0o640)
        for port in (32000, 33000, 33000):
            edge_vhost.configure(self.config, "fixture", "edge.example", port, self.root, self.run_nginx)
            self.assertEqual(self.legacy.read_text(), original.replace(":31000;", f":{port};"))
            self.assertEqual(self.legacy.stat().st_mode & 0o777, 0o640)
            self.assertIn(f":{port};", self.edge.read_text())
            self.assertNotIn("backend-ingress", self.legacy.read_text())
        self.assertEqual(self.calls, [["nginx", "-t"], ["systemctl", "reload", "nginx"]] * 2)
        self.assertEqual(self.include.stat().st_mode & 0o777, 0o600)
        self.assertNotIn(configured()["ingress_token"], self.edge.read_text())

    def test_new_app_has_no_public_legacy_vhost_and_cannot_downgrade(self):
        edge_vhost.configure(self.config, "fixture", "edge.example", 32000, self.root, self.run_nginx)
        self.assertFalse(self.legacy.exists())
        self.config.write_text(json.dumps(configured(ingress_required="false", ingress_token="")))
        with self.assertRaisesRegex(ValueError, "downgrade"):
            edge_vhost.validate(self.config, "fixture", "edge.example", self.root)
        self.config.unlink()
        with self.assertRaisesRegex(ValueError, "downgrade"):
            edge_vhost.validate(self.config, "fixture", "edge.example", self.root)

    def test_domain_collision_and_ambiguous_legacy_refuse_without_changes(self):
        for original in (configure_vhost.content("", "edge.example", 31000),
                         "server {\n    server_name ambiguous.example another.example;\n}"):
            self.legacy.write_text(original)
            with self.assertRaises(ValueError):
                edge_vhost.configure(self.config, "fixture", "edge.example", 32000, self.root, self.run_nginx)
            self.assertEqual(self.legacy.read_text(), original)
            self.assertFalse(self.edge.exists())
            self.assertFalse(self.include.exists())
        self.assertEqual(self.calls, [])

    def test_failed_nginx_check_restores_only_the_owned_files(self):
        original = configure_vhost.content("", "legacy.example", 31000)
        self.legacy.write_text(original)

        def failure(command, **kwargs):
            raise subprocess.CalledProcessError(1, command, stderr=b"private-output")

        with self.assertRaisesRegex(ValueError, "backend_edge_nginx_failed"):
            edge_vhost.configure(self.config, "fixture", "edge.example", 32000, self.root, failure)
        self.assertEqual(self.legacy.read_text(), original)
        self.assertFalse(self.edge.exists())
        self.assertFalse(self.include.exists())

    def refusing(self, failed, error):
        def run(command, **kwargs):
            self.calls.append(command)
            if command == failed:
                raise error
        return run

    def test_legacy_configure_restores_the_file_nginx_refused(self):
        # A current TLS vhost skips `certbot install`, so nothing later reloads nginx: a refused change must
        # not stay on disk, or the next deploy finds it unchanged while nginx keeps the old upstream.
        original = configure_vhost.content("", "legacy.example", 31000).encode() + b"# kept byte for byte\r\n"
        self.legacy.write_bytes(original)
        self.legacy.chmod(0o640)
        test, reload = ["nginx", "-t"], ["systemctl", "reload", "nginx"]
        for failed, error in ((test, subprocess.CalledProcessError(1, test, stderr=b"private-output")),
                              (test, subprocess.TimeoutExpired(test, 30)), (test, FileNotFoundError("nginx")),
                              (reload, subprocess.CalledProcessError(1, reload))):
            self.calls.clear()
            with self.subTest(failed=failed, error=type(error).__name__):
                with self.assertRaises(configure_vhost.NginxRejected):
                    configure_vhost.configure(self.legacy, "legacy.example", 32000, self.refusing(failed, error))
                self.assertEqual(self.calls, [test] if failed == test else [test, reload])
                self.assertEqual(self.legacy.read_bytes(), original)
                self.assertEqual(self.legacy.stat().st_mode & 0o777, 0o640)
                self.assertEqual(sorted(path.name for path in self.root.iterdir()),
                                 [".backend-edge.json", self.legacy.name])
        self.calls.clear()
        self.assertEqual(configure_vhost.configure(self.legacy, "legacy.example", 32000, self.run_nginx), "changed")
        self.assertEqual(self.calls, [test, reload])
        self.calls.clear()
        self.assertEqual(configure_vhost.configure(self.legacy, "legacy.example", 32000, self.run_nginx), "unchanged")
        self.assertEqual(self.calls, [])

    def test_a_refused_first_legacy_vhost_is_removed(self):
        test = ["nginx", "-t"]
        with self.assertRaises(configure_vhost.NginxRejected):
            configure_vhost.configure(self.legacy, "legacy.example", 32000,
                                      self.refusing(test, subprocess.CalledProcessError(1, test)))
        self.assertFalse(self.legacy.exists())
