# Deliberate input and geometry damage for extension contracts and atomicity.
from dataclasses import replace
import copy
import pytest
from hearth import Scene, Box, Frame, Binding, ContractError
from hearth.kernel import validate, Child, Plan, Choice, Limits
from hearth.blocks import state_of
from hearth.components.primitives import Volume
from hearth.environment import Terrain, gradient, wave
from hearth.persistence import semantic_digest, export_scene, load_scene, encode
from hearth_extensions import Polyline, Endpoint, Link, Boundary, IlluminateBoundary, Platform
from hearth_extensions.boundaries import BoundaryChain
from hearth_extensions.circulation import Trestles
from test_extension_geometry import bridge_scene


def snapshot(s):
    return (semantic_digest(s), copy.deepcopy(encode(s.nodes)), copy.deepcopy(encode(s.relations)), copy.deepcopy(encode(s.dependencies)), copy.deepcopy(encode(s.operations)), copy.deepcopy(s.reverse), copy.deepcopy(s.port_uses), copy.deepcopy(s.events))


def damage(s, p):
    record = s.blocks.pop(p)
    s.reverse[record.owner].remove(p)


@pytest.mark.parametrize('kind', ['support', 'guard', 'endpoint', 'headroom'])
def test_broken_bridge_geometry_is_diagnosed(kind):
    s = bridge_scene()
    if kind == 'support':
        p = s.nodes['/link/supports'].contract.rules[0].cells[0]
        damage(s, p)
        expected = 'support'
    elif kind == 'guard':
        p = next(iter(s.cells('/link/guards')))
        damage(s, p)
        expected = 'bridge-guard'
    elif kind == 'endpoint':
        a = s.view.port('/a', 'south').frame.origin
        p = (a[0], a[1] - 1, a[2])
        damage(s, p)
        expected = 'reachability'
    else:
        p = next(r for r in s.nodes['/link'].contract.rules if r.kind == 'clear').cells[len(s.nodes['/link'].contract.decisions['deck']) // 2]
        before = snapshot(s)
        with pytest.raises(ContractError, match='clearance'):
            s.place('blocked', Volume(Box(p, p), state_of('stone')))
        assert snapshot(s) == before
        return
    errors = validate(s, True)
    assert any(d.rule == expected and d.position is not None for d in errors)


@pytest.mark.parametrize('fault', ['endpoint', 'frame', 'kind'])
def test_invalid_connections_rollback_every_index(fault):
    s = bridge_scene()
    s.remove('/link')
    a, b = Endpoint('/a', 'south'), Endpoint('/b', 'north')
    pa = s.view.port('/a', 'south').frame.origin
    pb = s.view.port('/b', 'north').frame.origin
    curve = Polyline(((pa[0], pa[2]), (pa[0] + 3, pa[2]), (pb[0], pb[2])))
    expected = 'endpoint-frame'
    if fault == 'endpoint':
        curve = Polyline(((pa[0] + 1, pa[2]), (pb[0], pb[2])))
        expected = 'endpoint-disconnected'
    if fault == 'kind':
        a = Endpoint('/land', 'construction')
        expected = 'endpoint-kind'
    before = snapshot(s)
    with pytest.raises(ContractError, match=expected):
        s.place('bad', Link(a, b, '/land', curve=curve), bindings=(a.binding(), b.binding()))
    assert snapshot(s) == before


def test_connection_permission_overflow_is_atomic():
    s = Scene(0, domain=Box((-3, -4, -3), (12, 20, 12)))
    s.place('land', Terrain(s.domain, gradient(base=0), water_level=5, edit_limit=1))
    before = snapshot(s)
    columns = ((2, 0, 2, 4, True), (3, 0, 2, 4, True))
    with pytest.raises(ContractError, match='edit-limit'):
        s.place('piers', Trestles(columns), bindings=(Binding('/land', 'construction', 'fill'),))
    assert snapshot(s) == before


def test_connection_remove_and_regenerate_restore_water_and_membership():
    s = bridge_scene()
    s.remove('/link')
    before = snapshot(s)
    a, b = Endpoint('/a', 'south'), Endpoint('/b', 'north')
    comp = Link(a, b, '/land', rise=1)
    s.place('link', comp, bindings=(a.binding(), b.binding()))
    old = s.cells('/link', True)
    s.regenerate('/a', dependents=('/b', '/link'), replacements={'/link': replace(comp, covered=True, rise=0)})
    new = s.cells('/link', True)
    assert old != new
    assert all(s.inspect(p)['instance'] in s.nodes for p in new)
    assert all(not s.inspect(p)['instance'] or not s.inspect(p)['instance'].startswith('/link') for p in old - new)
    s.remove('/link')
    assert semantic_digest(s) == before[0]
    assert not any('/link' in a or '/link' in c for a, b, c in s.relations)
    assert not any(k.startswith('/link') for k in s.nodes)


def test_boundary_missing_coping_support_and_whole_removal(work):
    s = Scene(0, domain=Box((-5, -4, -5), (45, 24, 25)))
    s.place('land', Terrain(s.domain, gradient(base=3)))
    curve = Polyline.bezier(((0, 0), (9, 20), (26, 0), (37, 17)))
    s.place('boundary', BoundaryChain(curve))
    s.place('lamps', IlluminateBoundary('/boundary'))
    before = snapshot(s)
    with pytest.raises(ContractError):
        s.remove('/boundary')
    assert snapshot(s) == before
    changed = Polyline.bezier(((0, 1), (9, 19), (26, 1), (37, 18)))
    s.regenerate('/boundary', BoundaryChain(changed), dependents=('/lamps',))
    s.finalize()
    p = s.view.offers('/boundary', 'mount')[0].frame.origin
    assert p in s.cells('/boundary', True)
    export_scene(s, work / 'curve.litematic')
    t = load_scene(work / 'curve.litematic')
    assert t.inspect(p) == s.inspect(p)
    base = (p[0], p[1] - 1, p[2])
    damage(t, base)
    assert any(d.rule in ('support', 'enclosure') and d.position == base for d in validate(t, True))


def test_curve_boundary_rejects_incompatible_installation_frame():
    from hearth_extensions import BoundaryLantern
    s = Scene(0, domain=Box((-3, -4, -3), (25, 25, 25)))
    s.place('land', Terrain(s.domain, gradient(base=3)))
    s.place('wall', Boundary(Polyline(((0, 0), (20, 8)))))
    port = s.view.offers('/wall', 'mount')[0]
    before = snapshot(s)
    with pytest.raises(ContractError, match='port-alignment'):
        s.place('bad-lamp', BoundaryLantern(), frame=Frame(port.frame.origin, port.frame.turn + 1), bindings=(Binding('/wall', port.key),))
    assert snapshot(s) == before


def test_failed_choice_does_not_perturb_sibling_randomness():
    s = bridge_scene()
    s.remove('/link')
    a, b = Endpoint('/a', 'south'), Endpoint('/b', 'north')
    before = snapshot(s)
    bad = Link(a, b, '/land', fill_limit=0)
    with pytest.raises(ContractError):
        s.compose_choice(Choice('link', (Child('attempt', bad, bindings=(a.binding(), b.binding())),), lambda v, p: 0))
    assert snapshot(s) == before
    good = Link(a, b, '/land', rise=1)
    s.place('link', good, bindings=(a.binding(), b.binding()))
    assert semantic_digest(s) == semantic_digest(bridge_scene())
