# Building-craft contracts, actual damage, transforms, rollback and structural reload evidence.
from dataclasses import dataclass, replace
import copy
import json
import pytest
from hearth import Scene, Box, Frame, Binding, ContractError
from hearth.kernel import Capability, Contract, Plan, validate
from hearth.kernel.scene import Cell
from hearth.environment import Terrain, constant
from hearth.components.building import Building, Blueprint, RoomSpec
from hearth.components.roofcraft import RoofDesign, Dormer, ProfileRoof
from hearth.components.primitives import Volume
from hearth.lodges import hillside_home, gabled_hall, high_house, lodge_scene
from hearth.persistence import export_scene, load_scene, semantic_digest, encode
from hearth.quality_assessment import principal_signature
from hearth.blocks import state_of, name_of, parse


def make_house(bay=11, rise=(1, 2), turn=0, porch_depth=4):
    frame = Frame((30, 0, -20), turn)
    domain = frame.box(Box((-8, -4, -18), (30, 64, 28)))
    s = Scene(17, domain=domain)
    s.place('land', Terrain(domain, constant(3)))
    design = Blueprint((RoomSpec(0, 0, 0, 'living'), RoomSpec(0, 0, 1, 'bedroom')), bay=bay, balcony=True, porch_depth=porch_depth, roof_design=RoofDesign(rise), chimney=True)
    s.place('house', Building(design, '/land'), frame=frame)
    return s.finalize(), design


def damaged(scene, p, material=None):
    candidate = scene.fork()
    old = candidate.blocks.get(p)
    if old:
        candidate.reverse[old.owner].remove(p)
        del candidate.blocks[p]
    if material:
        candidate.blocks[p] = Cell(material, old.owner, old.operation, old.nbt)
        candidate.reverse[old.owner].add(p)
    return candidate


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


@pytest.mark.parametrize('bay,rise,depth', [(9, (1,), 3), (11, (1, 2), 4), (13, (2,), 5)])
def test_profile_porch_and_bay_boundaries(bay, rise, depth):
    s, design = make_house(bay, rise, porch_depth=depth)
    assert not validate(s, True)
    assert s.view.contract('/house').decisions['rooms'] == [(r.x, r.z, r.level, r.purpose) for r in design.normalized()]
    assert any(n.type == 'roof.dormer' for n in s.nodes.values())
    assert s.view.contract('/house/porch').decisions['balcony']
    assert s.cells('/house/porch')


@pytest.mark.parametrize('turn', range(4))
def test_rotated_dormer_ownership_and_complete_pair(turn, work):
    s, _ = make_house(turn=turn)
    for point in s.cells('/house/porch'):
        if name_of(s.blocks[point].state) != 'spruce_fence':
            continue
        _, properties = parse(s.blocks[point].state)
        for side, delta in {'north': (0, 0, -1), 'south': (0, 0, 1), 'east': (1, 0, 0), 'west': (-1, 0, 0)}.items():
            neighbor = tuple(point[i] + delta[i] for i in range(3))
            if properties[side] == 'true':
                assert s.view.state(neighbor) != 'minecraft:air'
            if name_of(s.view.state(neighbor)) == 'spruce_fence':
                assert properties[side] == 'true'
    windows = [n for n in s.nodes.values() if n.type == 'window.flush' and '/dormer-' in n.path]
    assert len(windows) >= 2
    a, b = [min(s.cells(n.path)) for n in windows[:2]]
    assert s.inspect(a)['state'] == s.inspect(b)['state']
    assert s.inspect(a)['instance'] != s.inspect(b)['instance']
    query = s.inspect(a)
    assert [n['type'] for n in query['chain']][:4] == ['window.flush', 'wall.dormer-face', 'roof.dormer', 'roof.weather-cover']
    assert query['chain'][-1]['id'] == '/house'
    assert any(kind == 'attached_to' for _, kind, _ in query['related'])
    path = work / 'crafted roof and balcony.litematic'
    export_scene(s, path)
    restored = load_scene(path)
    assert restored.inspect(a) == query
    assert encode(restored.nodes) == encode(s.nodes)
    assert restored.cells('/house', True) == s.cells('/house', True)
    assert {p: c.nbt for p, c in restored.blocks.items() if c.nbt} == {p: c.nbt for p, c in s.blocks.items() if c.nbt}


def test_actual_roof_glass_guard_support_and_lighting_damage():
    s, _ = make_house()
    roof = next(n for n in s.nodes.values() if n.type == 'roof.weather-cover')
    # A plain interior course outside the dormers, above every other solid cell.
    p = roof.frame.point((5, roof.contract.decisions['levels'][6], 1))
    # The ridge above this column is also removed: no projected cap may remain.
    t = s
    for y in range(p[1], roof.contract.envelope.hi[1] + 1):
        t = damaged(t, (p[0], y, p[2]))
    assert 'roof-cover' in {d.rule for d in validate(t, True)}
    glass = next(min(s.cells(n.path)) for n in s.nodes.values() if n.type == 'window.flush')
    assert 'enclosure' in {d.rule for d in validate(damaged(s, glass), True)}
    porch = s.nodes['/house/porch']
    railing = next(r.cells[0] for r in porch.contract.rules if r.kind == 'expected')
    assert 'expected-geometry' in {d.rule for d in validate(damaged(s, railing), True)}
    chimney = s.nodes['/house/chimney']
    p = chimney.frame.point((0, 2, 0))
    assert 'support' in {d.rule for d in validate(damaged(s, p), True)}
    room = '/house/room-0-0-0'
    t = s
    for p in s.cells(room, True):
        if name_of(s.blocks[p].state) == 'lantern':
            t = damaged(t, p)
    assert 'lighting' in {d.rule for d in validate(t, True)}


def test_balcony_door_and_furniture_access_remain_usable():
    s, _ = make_house()
    door = '/house/room-0-0-1/door-north'
    p = min(s.cells(door))
    broken = damaged(s, p, state_of('stone'))
    assert {'door-pair', 'reachability'} <= {d.rule for d in validate(broken, True)}
    node = next(n for n in s.nodes.values() if n.type.startswith('furnishing.group.'))
    port = s.view.port(node.path, 'use')
    with pytest.raises(ContractError, match='clearance|reachability'):
        s.place('blocked-use', Volume(port.region, state_of('stone')))
    assert '/blocked-use' not in s.nodes


def test_misaligned_dormer_and_permission_overflow_rollback():
    from hearth.components.roofcraft import RoofCover
    s = Scene(3, domain=Box((-3, -3, -3), (16, 30, 16)))
    s.place('land', Terrain(s.domain, constant(3)))
    s.place('cover', RoofCover(11, 11, 'deepslate_tile', RoofDesign(), populate=False), frame=Frame((0, 4, 0)))
    roof = s.nodes['/cover']
    port = s.view.offers('/cover', 'roof-installation')[0]
    before = snapshot(s)
    with pytest.raises(ContractError, match='dormer-interface'):
        s.place('bad-frame', Dormer(), frame=Frame(port.frame.origin, (port.frame.turn + 1) % 4), bindings=(Binding(roof.path, port.key),))
    assert snapshot(s) == before

    @dataclass
    class Spill:

        def capability(self):
            return Capability('probe.spill', ('dormer',))

        def negotiate(self, c, params):
            return Contract(Box.enclosing(port.frame.inverse(q) for q in roof.contract.envelope.corners()))

        def realize(self, c, contract):
            plan = Plan()
            plan.block(port.frame.inverse(roof.frame.point((5, roof.contract.decisions['levels'][6], 1))), state_of('air'))
            return plan

    with pytest.raises(ContractError, match='write-authority'):
        s.place('overflow', Spill(), frame=port.frame, bindings=(Binding(roof.path, port.key),))
    assert snapshot(s) == before
    # A separate facade program can discover a mount and install without private geometry.
    s.place('external-dormer', Dormer(), frame=port.frame, bindings=(Binding('/cover', port.key),))
    s.finalize()
    window = '/external-dormer/face/window'
    point = min(s.cells(window))
    chain = [n['id'] for n in s.inspect(point)['chain']]
    assert chain == [window, '/external-dormer/face', '/external-dormer', '/cover']
    assert s.cells(window) <= s.cells('/cover', descendants=True)


def test_regeneration_removes_old_dormer_cells_and_structure():
    s, design = make_house()
    old = {p for p in s.nodes if '/dormer-' in p}
    assert old
    replacement = replace(design, roof_design=RoofDesign((1, 2), 0))
    s.regenerate('/house', Building(replacement, '/land'))
    s.finalize()
    assert not old.intersection(s.nodes)
    assert all(c.owner not in old for c in s.blocks.values())
    assert all(not {a, c}.intersection(old) for a, _, c in s.relations)
    assert all(p in s.blocks and s.blocks[p].owner == owner for owner, cells in s.reverse.items() for p in cells)


def test_roof_policy_substitution_without_parent_change():
    s, design = make_house()
    from hearth.components.roofs import Roof

    @dataclass(frozen=True)
    class IndependentPolicy:

        def heights(self, span):
            return (0, (span + 1) // 2)

        def component(self, width, depth, axis, material):
            return Roof(width, depth, axis, material)

    s.regenerate('/house', Building(replace(design, roof_design=IndependentPolicy()), '/land'))
    s.finalize()
    assert any(n.type == 'roof.gable' for n in s.nodes.values())
    assert not any(n.type == 'roof.profile' for n in s.nodes.values())
    assert s.nodes['/house'].contract.decisions['rooms'] == [(r.x, r.z, r.level, r.purpose) for r in design.normalized()]


@pytest.mark.parametrize('client', (hillside_home, gabled_hall, high_house))
@pytest.mark.parametrize('seed', (0, 6))
def test_complete_new_compositions(client, seed):
    s = lodge_scene(client(seed), seed, seed)
    assert s.completed and not validate(s, True)
    assert len([n for n in s.nodes.values() if n.type == 'roof.dormer']) >= 1
    assert len([n for n in s.nodes.values() if n.type == 'plant.conifer']) >= 3


def test_deterministic_architecture_and_unrelated_branch():
    a, _ = make_house()
    b, _ = make_house()
    assert semantic_digest(a) == semantic_digest(b)
    signature = principal_signature(a)
    # A read-only addition outside the building changes neither blocks nor architectural decisions.
    a.place('marker', Volume(Box((50, 20, 4), (50, 20, 4)), state_of('stone')))
    assert principal_signature(a) == signature
    assert a.cells('/house', True) == b.cells('/house', True)


def test_heavy_shaft_on_piers_has_continuous_bounded_bearing():
    from hearth.assessment import adaptation_expression
    field, water = adaptation_expression(0)
    s = lodge_scene(hillside_home(0), 0, 0, field, water, garden=False)
    course = s.nodes['/building/chimney-bearing']
    assert course.contract.decisions['fill'] > 0
    assert course.contract.decisions['span'] == 2
    assert all(s.view.supports(p) for p in course.contract.rules[0].cells)
    p = course.contract.rules[0].cells[-1]
    assert 'support' in {d.rule for d in validate(damaged(s, p), True)}


def test_bearing_rejects_missing_support_and_rolls_back():
    from hearth.adapters.bearing import BearingCourse
    s = Scene()
    before = snapshot(s)
    with pytest.raises(ContractError, match='bearing-span'):
        s.place('floating', BearingCourse(3, 2))
    assert snapshot(s) == before
