import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from types import SimpleNamespace
import cleanup

class RetentionTests(unittest.TestCase):
    def setUp(self):
        self.temp=tempfile.TemporaryDirectory()
        self.root=Path(self.temp.name).resolve()
        self.now=1800000000
    def tearDown(self): self.temp.cleanup()
    def file(self, name, age):
        p=self.root/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_text('keep or delete')
        os.utime(p,(self.now-age,self.now-age));return p
    def test_preview_never_deletes(self):
        p=self.file('day/old.txt',8*86400)
        r=cleanup.clean(self.root,False,self.now)
        self.assertEqual(r['eligible_files'],1);self.assertTrue(p.exists())
    def test_exact_seven_days_and_recent_are_preserved(self):
        old=self.file('old.txt',7*86400+1);boundary=self.file('boundary.txt',7*86400);fresh=self.file('fresh.mp4',60)
        cleanup.clean(self.root,False,self.now,True)
        self.assertFalse(old.exists());self.assertTrue(boundary.exists());self.assertTrue(fresh.exists());self.assertTrue(self.root.exists())
    def test_symlinks_and_outside_uploads_are_preserved(self):
        outside=self.file('uploads/source.jpg',10*86400)
        ai=self.root/'aiboxfile';ai.mkdir()
        (ai/'link.jpg').symlink_to(outside);(ai/'dir').symlink_to(outside.parent,target_is_directory=True)
        cleanup.clean(ai,False,self.now,True)
        self.assertTrue(outside.exists());self.assertTrue((ai/'link.jpg').is_symlink())
    def test_root_symlink_rejected(self):
        link=self.root/'linked';real=self.root/'real';real.mkdir();link.symlink_to(real,target_is_directory=True)
        with self.assertRaises(RuntimeError):cleanup.clean(link,False,self.now,True)
    def test_active_log_never_deleted(self):
        p=self.file('out-0.log',30*86400)
        cleanup.clean(self.root,True,self.now,True);self.assertTrue(p.exists())
    def test_only_our_old_archives_removed(self):
        old=self.file('out-0.log-20260101-'+str(self.now-8*86400)+'.gz',0)
        fresh=self.file('out-0.log-20260102-'+str(self.now-86400),30*86400)
        unrelated=self.file('backup.sql.gz',30*86400)
        cleanup.clean(self.root,True,self.now,True)
        self.assertFalse(old.exists());self.assertTrue(fresh.exists());self.assertTrue(unrelated.exists())
    def test_empty_directories_preserved(self):
        (self.root/'empty').mkdir();cleanup.clean(self.root,False,self.now,True)
        self.assertTrue((self.root/'empty').is_dir())
    def test_refreshed_ai_file_preserved(self):
        p=self.file('updated.txt',9*86400);os.utime(p,(self.now,self.now))
        cleanup.clean(self.root,False,self.now,True);self.assertTrue(p.exists())
    def test_nonroot_config_rejected(self):
        cfg=self.root/'rotate.conf';cfg.write_text('daily')
        with patch.object(Path,'lstat',return_value=SimpleNamespace(st_mode=0o100644,st_uid=501)):
            with self.assertRaises(RuntimeError): cleanup.validate_config(cfg)
    def test_writable_config_rejected(self):
        cfg=self.root/'rotate.conf';cfg.write_text('daily')
        with patch.object(Path,'lstat',return_value=SimpleNamespace(st_mode=0o100666,st_uid=0)):
            with self.assertRaises(RuntimeError): cleanup.validate_config(cfg)
    def test_logrotate_zero_exit_with_error_rejected(self):
        result=SimpleNamespace(returncode=0,stdout='',stderr='error: Ignoring config because owner is wrong\nHandling 0 logs\n')
        with patch.object(cleanup.subprocess,'run',return_value=result):
            with self.assertRaises(RuntimeError): cleanup.run_logrotate(['logrotate','-d','config'])
    def test_logrotate_debug_warning_allowed(self):
        result=SimpleNamespace(returncode=0,stdout='',stderr='WARNING: logrotate in debug mode does nothing\nHandling 1 logs\n')
        with patch.object(cleanup.subprocess,'run',return_value=result):
            cleanup.run_logrotate(['logrotate','-d','config'])

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