"""Run only against private temporary fixtures, never production logs."""
import json
import io
from contextlib import redirect_stdout, redirect_stderr
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
import unittest
from unittest.mock import patch

import cleanup_logs as tool

SCRIPT = Path(__file__).with_name("cleanup_logs.py")


class CleanupTests(unittest.TestCase):
    def setUp(self):
        self.work = tempfile.TemporaryDirectory(prefix="log-retention-test-")
        self.addCleanup(self.work.cleanup)
        self.root = Path(self.work.name)
        self.logs = self.root / "logs"
        self.logs.mkdir()
        self.now = time.time_ns()
        self.old = self.now - 9 * tool.DAY_NS
        for name in ("old.log", "fresh.log", "keep.txt", ".hidden.log", "UPPER.LOG"):
            path = self.logs / name
            path.write_bytes(b"old\n")
            stamp = self.now if name == "fresh.log" else self.old
            os.utime(path, ns=(stamp, stamp))
        (self.logs / "nested.log").mkdir()
        (self.logs / "nested.log" / "inside.log").write_bytes(b"keep")
        (self.logs / "link.log").symlink_to("keep.txt")
        (self.logs / "broken.log").symlink_to("absent")
        hard = self.logs / "hard.log"
        hard.write_bytes(b"keep")
        os.utime(hard, ns=(self.old, self.old))
        os.link(hard, self.root / "hard-copy")

    def run_cli(self, *args):
        return subprocess.run([sys.executable, str(SCRIPT), *map(str, args)],
                              capture_output=True, text=True)

    def test_preview_apply_repeat(self):
        result = self.run_cli(self.logs, "--days", 7)
        self.assertEqual((result.returncode, result.stderr), (0, ""))
        rows = [json.loads(line) for line in result.stdout.splitlines()]
        self.assertEqual(rows, [
            {"action": "would_delete", "name": "old.log", "bytes": 4},
            {"mode": "preview", "selected": 1, "deleted": 0},
        ])
        self.assertEqual((self.logs / "old.log").read_bytes(), b"old\n")
        result = self.run_cli(self.logs, "--days", 7, "--apply")
        self.assertEqual((result.returncode, result.stderr), (0, ""))
        self.assertEqual(json.loads(result.stdout.splitlines()[-1]),
                         {"mode": "apply", "selected": 1, "deleted": 1})
        self.assertFalse((self.logs / "old.log").exists())
        for name in ("fresh.log", "keep.txt", ".hidden.log", "UPPER.LOG", "hard.log"):
            self.assertEqual((self.logs / name).read_bytes(), b"old\n" if name != "hard.log" else b"keep")
        self.assertTrue((self.logs / "link.log").is_symlink())
        self.assertTrue((self.logs / "broken.log").is_symlink())
        self.assertEqual((self.logs / "nested.log" / "inside.log").read_bytes(), b"keep")
        result = self.run_cli(self.logs, "--days", 7, "--apply")
        self.assertEqual(result.returncode, 0)
        self.assertEqual(json.loads(result.stdout),
                         {"mode": "apply", "selected": 0, "deleted": 0})

    def test_strict_cutoff_and_future(self):
        exact = self.logs / "exact.log"
        exact.write_bytes(b"boundary")
        cutoff = self.now - 7 * tool.DAY_NS
        os.utime(exact, ns=(cutoff, cutoff))
        future = self.logs / "future.log"
        future.write_bytes(b"future")
        os.utime(future, ns=(self.now + tool.DAY_NS, self.now + tool.DAY_NS))
        self.assertEqual([p.name for p, _ in tool.select_logs(self.logs, 7, self.now)],
                         ["old.log"])

    def test_changed_file_rejected(self):
        path, previous = tool.select_logs(self.logs, 7, self.now)[0]
        path.write_bytes(b"changed content")
        with self.assertRaisesRegex(ValueError, "changed since scan"):
            tool.apply_one(path, previous)
        self.assertEqual(path.read_bytes(), b"changed content")

    def test_changed_to_symlink_rejected(self):
        path, previous = tool.select_logs(self.logs, 7, self.now)[0]
        path.unlink()
        path.symlink_to("keep.txt")
        with self.assertRaisesRegex(ValueError, "changed since scan"):
            tool.apply_one(path, previous)
        self.assertTrue(path.is_symlink())

    def test_cli_errors_and_help(self):
        for value in ("0", "-1", "36501", "1.5"):
            result = self.run_cli(self.logs, "--days", value)
            self.assertEqual(result.returncode, 2)
            self.assertEqual(result.stdout, "")
        for path in (self.root / "absent", self.logs / "keep.txt"):
            result = self.run_cli(path, "--days", 7, "--apply")
            self.assertEqual(result.returncode, 1)
            self.assertEqual(result.stdout, "")
        result = self.run_cli("--help")
        self.assertEqual(result.returncode, 0)
        self.assertIn("--apply", result.stdout)

    def test_scan_and_unlink_errors(self):
        with patch.object(tool.os, "scandir", side_effect=PermissionError("scan denied")):
            with self.assertRaises(PermissionError):
                tool.select_logs(self.logs, 7, self.now)
        path, previous = tool.select_logs(self.logs, 7, self.now)[0]
        with patch.object(Path, "unlink", side_effect=PermissionError("delete denied")):
            with self.assertRaises(PermissionError):
                tool.apply_one(path, previous)
        self.assertTrue(path.exists())

    def test_partial_failure_is_reported(self):
        second = self.logs / "z.log"
        second.write_bytes(b"keep")
        os.utime(second, ns=(self.old, self.old))
        original = tool.apply_one

        def fail_second(path, info):
            if path.name == "z.log":
                raise PermissionError("simulated deletion failure")
            return original(path, info)

        output, errors = io.StringIO(), io.StringIO()
        args = [str(SCRIPT), str(self.logs), "--days", "7", "--apply"]
        with patch.object(sys, "argv", args), patch.object(tool, "apply_one", fail_second):
            with redirect_stdout(output), redirect_stderr(errors):
                code = tool.main()
        self.assertEqual(code, 1)
        self.assertIn("deleted=1", errors.getvalue())
        self.assertEqual(json.loads(output.getvalue()),
                         {"action": "deleted", "name": "old.log", "bytes": 4})
        self.assertFalse((self.logs / "old.log").exists())
        self.assertTrue(second.exists())


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