import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from deployment_kit import models, model_repair
from deployment_kit.core import KitError, run, write_json, sha256
from test_models import candidate
from helpers import IID


class RepairTests(unittest.TestCase):
    def setUp(self):
        t=tempfile.TemporaryDirectory(); self.addCleanup(t.cleanup)
        self.root=Path(t.name); self.src=self.root/'old'; self.dst=self.root/'new'
        m=candidate(self.src)
        m['smoke']='NOT_RUN'
        c=models.pbtxt(m['profile']).replace('\n]\ninstance_group',',\n]\ninstance_group',1)
        (self.src/'config.pbtxt').write_text(c)
        m['config_sha256']=sha256(self.src/'config.pbtxt')
        write_json(self.src/'manifest.json',m)
        (self.src/'smoke-repository').mkdir()

    def test_new_candidate_preserves_source_and_engine(self):
        before={p.name:p.read_bytes() for p in self.src.iterdir() if p.is_file()}
        model_repair.repair(self.src,self.dst)
        self.assertFalse(self.dst.exists())
        model_repair.repair(self.src,self.dst,apply=True)
        m=models.candidate_manifest(self.dst)
        self.assertEqual(m['smoke'],'NOT_RUN')
        self.assertEqual((self.dst/'model.plan').read_bytes(),before['model.plan'])
        self.assertNotEqual((self.dst/'model.plan').stat().st_ino,(self.src/'model.plan').stat().st_ino)
        self.assertFalse((self.dst/'smoke-repository').exists())
        for name,data in before.items():self.assertEqual((self.src/name).read_bytes(),data)
        with self.assertRaises(KitError):model_repair.repair(self.src,self.dst,apply=True)

    def test_tampered_engine_rejected(self):
        (self.src/'model.plan').write_bytes(b'changed')
        with self.assertRaises(KitError):model_repair.repair(self.src,self.dst,apply=True)
        self.assertFalse(self.dst.exists())

    def test_other_config_change_rejected_even_with_updated_hash(self):
        c=self.src/'config.pbtxt';c.write_text(c.read_text().replace('count: 8','count: 7'))
        m=json.loads((self.src/'manifest.json').read_text());m['config_sha256']=sha256(c)
        write_json(self.src/'manifest.json',m)
        with self.assertRaises(KitError):model_repair.repair(self.src,self.dst)

    def test_partial_output_preserved_after_copy_error(self):
        with patch.object(model_repair.shutil,'copyfile',side_effect=OSError('disk')):
            with self.assertRaises(OSError):model_repair.repair(self.src,self.dst,apply=True)
        self.assertTrue((self.dst/'repair-intent.json').exists())
        self.assertFalse((self.dst/'manifest.json').exists())


class DiagnosticTests(unittest.TestCase):
    def test_stderr_merge_opt_in(self):
        script='import sys; print("out"); print("err",file=sys.stderr)'
        self.assertNotIn('err',run([sys.executable,'-c',script])[1])
        merged=run([sys.executable,'-c',script],merge_output=True)[1]
        self.assertIn('err',merged);self.assertIn('out',merged)

    def test_failure_capture_before_cleanup(self):
        with tempfile.TemporaryDirectory() as td:
            root=Path(td); d=root/'candidate';candidate(d)
            events=[]
            def docker(*args,**kwargs):
                events.append(args[0])
                if args[0]=='create':return 0,'a'*64
                if args[0]=='exec':raise KitError('MODEL_NOT_READY','fixture')
                if args[0]=='logs':
                    self.assertTrue(kwargs['merge_output'])
                    return 0,'protobuf parse error on stderr'
                if args[0]=='inspect':return 0,json.dumps({'Running':False,'ExitCode':1,'OOMKilled':False})
                return 0,''
            with patch.object(models,'docker',side_effect=docker),patch.object(models,'image_id',return_value=IID),patch.object(models,'cleanup_helper',side_effect=lambda *a:events.append('cleanup')):
                with self.assertRaises(KitError):models.verify_candidate.__wrapped__(d,IID,apply=True)
            self.assertLess(events.index('logs'),events.index('cleanup'))
            self.assertIn('protobuf',(d/'smoke-triton.log').read_text())
            self.assertEqual((d/'smoke-triton.log').stat().st_mode&0o777,0o600)
            self.assertEqual(json.loads((d/'smoke-failure.json').read_text())['state']['ExitCode'],1)

    def test_capture_failure_does_not_replace_original(self):
        with tempfile.TemporaryDirectory() as td:
            with patch.object(models,'docker',side_effect=OSError('unavailable')),patch.object(models,'write_json',side_effect=OSError('full')):
                models.preserve_smoke_failure(Path(td),'a'*64,KitError('MODEL_NOT_READY','original'))
