import copy
import json
import tempfile
import unittest
from pathlib import Path
from reference_planner import ValidationError, load_json, plan_changes


def fixture(prefixes=None, revision='obs-001', tenant='red', device='leaf-a'):
    return {'complete': True, 'revision': revision, 'devices': [
        {'tenant': tenant, 'device_id': device,
         'allowed_prefixes': ['192.0.2.0/24'] if prefixes is None else prefixes}]}


class PlannerTests(unittest.TestCase):
    def plan(self, wanted=None, actual=None, **kwargs):
        return plan_changes(fixture() if wanted is None else wanted,
                            fixture() if actual is None else actual,
                            expected_observed_revision='obs-001', **kwargs)

    def test_no_change(self):
        self.assertEqual(self.plan()['changes'], [])
    def test_add(self):
        result = self.plan(fixture(['192.0.2.0/24', '198.51.100.0/24']))
        self.assertEqual(result['changes'][0]['add'], ['198.51.100.0/24'])
    def test_remove_rejected(self):
        with self.assertRaises(ValidationError): self.plan(fixture([]))
    def test_remove_approved(self):
        self.assertEqual(self.plan(fixture([]), allow_removals=True)['changes'][0]['remove'], ['192.0.2.0/24'])
    def test_incomplete_observation(self):
        data=fixture(); data['complete']=False
        with self.assertRaises(ValidationError): self.plan(actual=data, allow_removals=True)
    def test_incomplete_desired(self):
        data=fixture(); data['complete']=False
        with self.assertRaises(ValidationError): self.plan(wanted=data)
    def test_string_completeness(self):
        data=fixture(); data['complete']='true'
        with self.assertRaises(ValidationError): self.plan(actual=data)
    def test_integer_completeness(self):
        data=fixture(); data['complete']=1
        with self.assertRaises(ValidationError): self.plan(actual=data)
    def test_host_bits_rejected(self):
        with self.assertRaises(ValidationError): self.plan(fixture(['192.0.2.17/24']))
    def test_duplicate_normalized_prefix(self):
        with self.assertRaises(ValidationError): self.plan(fixture(['2001:db8::/32','2001:0db8::/32']))
    def test_malformed_prefix(self):
        with self.assertRaises(ValidationError): self.plan(fixture(['not-a-prefix']))
    def test_scoped_duplicate_rejected(self):
        data=fixture(); data['devices'].append(copy.deepcopy(data['devices'][0]))
        with self.assertRaises(ValidationError): self.plan(data)
    def test_tenants_can_overlap(self):
        data=fixture(); data['devices'].append(fixture(tenant='blue')['devices'][0])
        self.assertEqual(self.plan(data,copy.deepcopy(data))['changes'],[])
    def test_scope_mismatch(self):
        data=fixture(); data['devices']=[]
        with self.assertRaises(ValidationError): self.plan(data,allow_removals=True)
    def test_stale_revision(self):
        with self.assertRaises(ValidationError): self.plan(actual=fixture(revision='obs-000'))
    def test_deterministic_prefix_order(self):
        a=fixture(['198.51.100.0/24','2001:db8::/32','192.0.2.0/24'])
        b=copy.deepcopy(a); b['devices'][0]['allowed_prefixes'].reverse()
        self.assertEqual(self.plan(a),self.plan(b))
    def test_second_run_no_side_effect_plan(self):
        desired=fixture(['192.0.2.0/24','198.51.100.0/24'],revision='desired-002')
        first=self.plan(desired)
        observed=copy.deepcopy(desired); observed['revision']='obs-001'
        self.assertTrue(first['changes'])
        self.assertEqual(self.plan(desired,observed)['changes'],[])
    def test_inputs_not_mutated(self):
        desired=fixture(['198.51.100.0/24','192.0.2.0/24']); old=copy.deepcopy(desired)
        self.plan(desired); self.assertEqual(desired,old)
    def test_unknown_field_rejected(self):
        data=fixture(); data['sucess']=True
        with self.assertRaises(ValidationError): self.plan(data)
    def test_allow_remove_type(self):
        with self.assertRaises(ValidationError): self.plan(allow_removals='false')
    def test_missing_cidr(self):
        with self.assertRaises(ValidationError): self.plan(fixture(['192.0.2.0']))
    def test_duplicate_json_key(self):
        with tempfile.TemporaryDirectory() as tmp:
            p=Path(tmp)/'bad.json'; p.write_text('{"complete":true,"complete":false}')
            with self.assertRaises(ValidationError): load_json(str(p))


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