# Public habitable roof-space interfaces and actual circulation/enclosure validation.
import pytest
from hearth import Scene, Box, Frame
from hearth.environment import Terrain, constant
from hearth.components.interior import Room
from hearth.components.roofcraft import ProfileRoof
from hearth.components import Blueprint, RoomSpec
from hearth.components.roofcraft import RoofDesign
from hearth.lodges import lodge_scene
from hearth.kernel import validate


@pytest.mark.parametrize('bay', (9, 11, 13))
@pytest.mark.parametrize('axis', ('x', 'z'))
def test_habitable_roof_keeps_stair_access_and_actual_room_guarantees(bay, axis):
    design = Blueprint((RoomSpec(0, 0, 0, 'living'),), bay=bay, roof_axis=axis, roof_design=RoofDesign((1, 2)), attic_purpose='bedroom')
    scene = lodge_scene(design, 4, garden=False)
    assert not validate(scene, True)
    room = scene.nodes['/building/roof-0/room']
    assert room.type == 'room.loft.bedroom'
    assert len(room.contract.decisions['furniture']) >= 2
    assert room.contract.decisions['lights']
    assert any(r.kind == 'enclosed-volume' for r in room.contract.rules)


def test_public_roof_and_upper_floor_interfaces():
    scene = Scene(51, domain=Box((-4, -2, -4), (18, 35, 18)))
    scene.place('land', Terrain(scene.domain, constant(0)))
    scene.place('room', Room('living', stair_up=True, upper_open=True), frame=Frame((0, 1, 0)), complete=False)
    access = scene.view.offers('/room', 'upper-floor-access')
    assert access, 'An enclosing composition must discover the actual stair aperture'
    roof = ProfileRoof(11, 11)
    assert 'roof-interior' in roof.capability().offers


from dataclasses import dataclass, replace
from hearth import Binding, ContractError
from hearth.kernel import Capability, Contract, Plan
from hearth.blocks import name_of, state_of, parse
from hearth.components import Building
from hearth.components.lofts import InhabitedRoof, RoofPlatform, LoftLight
from hearth.components.primitives import Volume
from hearth.persistence import export_scene, load_scene, encode, semantic_digest
from hearth.quality_assessment import audit_pair, principal_signature
from test.test_building_quality import damaged, snapshot
from examples.roof_home import roof_home


def loft_scene(turn=0, purpose='bedroom'):
    frame = Frame((24, 0, -16), turn)
    domain = frame.box(Box((-8, -3, -18), (36, 55, 30)))
    scene = Scene(84, domain=domain)
    scene.place('land', Terrain(domain, constant(2)))
    design = Blueprint((RoomSpec(0, 0, 0, 'living'), RoomSpec(1, 0, 0, 'kitchen')), bay=11, roof_axis='x', roof_design=RoofDesign((1, 2)), attic_purpose=purpose, porch_bays=2, porch_depth=4)
    scene.place('house', Building(design, '/land'), frame=frame)
    return scene.finalize(), design


@pytest.mark.parametrize('turn', range(4))
def test_loft_transform_provenance_and_complete_pair(turn, work):
    scene, design = loft_scene(turn)
    loft = next(n for n in scene.nodes.values() if n.type == 'room.loft.bedroom')
    station = next(n for n in scene.nodes.values() if n.path.startswith(loft.path) and n.type == 'furnishing.group.bedroom')
    point = next(p for p in scene.cells(station.path) if name_of(scene.view.state(p)) == 'red_bed')
    chain = scene.inspect(point)['chain']
    assert [n['type'] for n in chain][:3] == ['furnishing.group.bedroom', 'room.loft.bedroom', 'roof.profile']
    assert chain[-1]['id'] == '/house'
    assert any(a == loft.path and kind == 'supported_by' for a, kind, b in scene.inspect(point)['related'])
    assert any(a == loft.path and kind == 'connected_to' for a, kind, b in scene.relations)
    roof = next(n for n in scene.nodes.values() if n.type == 'roof.profile')
    offer = scene.view.offers(roof.path, 'roof-interior')[0]
    assert offer.frame == loft.frame
    lantern = next(n for n in scene.nodes.values() if n.type == 'light.roof-hanger')
    top = lantern.contract.decisions['top']
    assert scene.view.supports(lantern.frame.point((0, top, 0)))
    assert all(name_of(scene.view.state(lantern.frame.point((0, y, 0)))) == 'chain' for y in range(1, top))
    stair = scene.nodes['/house/room-0-0-0/stairs']
    tread = stair.frame.point((0, 0, 0))
    facing = parse(scene.view.state(tread))[1]['facing']
    assert facing == ('south', 'west', 'north', 'east')[stair.frame.turn % 4]
    path = work / 'usable roof.litematic'
    export_scene(scene, path)
    audit = audit_pair(scene, path)
    assert audit['nodes'] == len(scene.nodes) and audit['inventories'] > 0
    restored = load_scene(path)
    assert restored.inspect(point) == scene.inspect(point)
    assert restored.cells(loft.path, True) == scene.cells(loft.path, True)
    assert not validate(restored, True)


@pytest.mark.parametrize('seed', range(8))
def test_new_lightweight_roof_program_includes_required_lofts(seed):
    scene = lodge_scene(roof_home(seed), seed, seed, garden=False)
    lofts = [n for n in scene.nodes.values() if n.type.startswith('room.loft.')]
    covers = [n for n in scene.nodes.values() if n.type == 'roof.inhabited']
    assert lofts and len(lofts) == len(covers)
    assert all(n.contract.decisions['purpose'] == roof_home(seed).attic_purpose for n in lofts)
    assert not validate(scene, True)


@pytest.mark.parametrize('damage,expected', [('wall', 'enclosure-leak'), ('support', 'support'), ('tread', 'support'), ('bed', 'bed-pair'), ('collar', 'expected-geometry'), ('guard', 'expected-geometry')])
def test_actual_loft_damage_detected(damage, expected):
    scene, _ = loft_scene()
    if damage == 'wall':
        part = scene.nodes['/house/roof-floor-0/knees']
        q = part.frame.point((5, 1, 0))
        # Remove the complete two-cell eave wall thickness, not only its inner beam.
        scene = damaged(scene, part.frame.point((5, 1, -1)))
    elif damage == 'support':
        part = scene.nodes['/house/roof-floor-0/floor']
        q = next(r.cells[0] for r in part.contract.rules if r.kind == 'support')
    elif damage == 'tread':
        part = scene.nodes['/house/room-0-0-0/stairs']
        q = next(p for p in scene.cells(part.path) if name_of(scene.view.state(p)) == 'spruce_stairs')
    elif damage == 'bed':
        q = next(p for p in scene.cells('/house/roof-0/room', True) if name_of(scene.view.state(p)) == 'red_bed')
    else:
        kind = 'timber.roof-collars' if damage == 'collar' else 'circulation.aperture-guard'
        part = next(n for n in scene.nodes.values() if n.type == kind)
        assert scene.cells(part.path)
        q = min(scene.cells(part.path))
    diagnostics = validate(damaged(scene, q), True)
    assert expected in {d.rule for d in diagnostics}
    assert all(d.component and d.position is not None for d in diagnostics if d.rule == expected)


def test_loft_light_sources_function_and_nbt_checked_from_blocks():
    scene, _ = loft_scene()
    loft = scene.nodes['/house/roof-0/room']
    unlit = scene
    for n in scene.nodes.values():
        if n.type == 'light.roof-hanger':
            unlit = damaged(unlit, n.frame.origin)
    assert any(d.rule == 'lighting' and d.component == loft.path for d in validate(unlit, True))
    unfurnished = scene
    for p in scene.cells(loft.path, True):
        if name_of(scene.view.state(p)) == 'red_bed':
            unfurnished = damaged(unfurnished, p)
    assert any(d.rule == 'furnishing' and d.component == loft.path for d in validate(unfurnished, True))
    # Deliberately corrupt NBT in a fork, without weakening the public write path.
    corrupt = scene.fork()
    from copy import deepcopy
    q = next(p for p in scene.cells(loft.path, True) if scene.blocks[p].nbt)
    data = deepcopy(scene.blocks[q].nbt)
    data['Items'][0]['count'] = 80
    corrupt.blocks[q] = replace(corrupt.blocks[q], nbt=data)
    assert any(d.rule == 'inventory' and d.position == q for d in validate(corrupt, True))


def test_stair_and_room_routes_reject_late_decoration_atomically():
    scene, _ = loft_scene()
    stair = scene.nodes['/house/room-0-0-0/stairs']
    point = next(p for r in stair.contract.rules if r.kind == 'clear' for p in r.cells if name_of(scene.view.state(p)) == 'air')
    before = snapshot(scene)
    with pytest.raises(ContractError, match='clearance'):
        scene.place('late-decoration', Volume(Box(point, point), state_of('stone')))
    assert snapshot(scene) == before
    loft = scene.nodes['/house/roof-0/room']
    landing = scene.view.port(loft.path, 'landing').frame.origin
    with pytest.raises(ContractError, match='clearance'):
        scene.place('blocked-landing', Volume(Box(landing, landing), state_of('stone')))
    assert snapshot(scene) == before


@dataclass(frozen=True)
class RoofIntrusion:
    """A deliberately invalid consumer that attempts an unauthorized roof edit."""

    def capability(self):
        return Capability('test.roof-intrusion', ('roof-room',))

    def negotiate(self, ctx, parameters):
        port = ctx.view.port(ctx.bindings[0].host, ctx.bindings[0].port)
        return Contract(Box.enclosing(ctx.local(q) for q in port.region.corners()))

    def realize(self, ctx, contract):
        p = Plan()
        p.fill(contract.envelope, state_of('stone'), 'illegal-interior')
        return p


def test_roof_interior_has_no_replacement_authority_and_bad_frames_roll_back():
    scene = Scene(13, domain=Box((-5, -3, -5), (20, 40, 20)))
    scene.place('land', Terrain(scene.domain, constant(0)))
    scene.place('room', Room('living', upper_open=False), frame=Frame((0, 1, 0)))
    scene.place('roof', ProfileRoof(11, 11), frame=Frame((0, 8, 0)))
    offer = scene.view.offers('/roof', 'roof-interior')[0]
    before = snapshot(scene)
    with pytest.raises(ContractError, match='write-authority|permission-volume|clearance'):
        scene.place('intrusion', RoofIntrusion(), frame=offer.frame, bindings=(Binding('/roof', offer.key),))
    assert snapshot(scene) == before
    with pytest.raises(ContractError, match='inhabited-roof-interface'):
        scene.place('wrong-port', InhabitedRoof(11, 11, 'z', 'deepslate_tile'), frame=Frame((0, 7, 0), 1), bindings=(Binding('/roof', offer.key, 'connect'),))
    assert snapshot(scene) == before


def test_branch_regeneration_retires_lofts_and_restores_consistent_indexes(work):
    scene, design = loft_scene()
    old_nodes = {p for p in scene.nodes if '/roof-' in p}
    old_cells = scene.cells('/house/roof-0/room', True)
    replacement = Building(replace(design, attic_purpose=None), '/land')
    scene.regenerate('/house', replacement)
    assert old_cells and not scene.cells('/house/roof-0/room', True)
    assert not any(n.type.startswith('room.loft.') for n in scene.nodes.values())
    retired = old_nodes - set(scene.nodes)
    assert retired and not any(c.owner in retired for c in scene.blocks.values())
    assert not any(a in retired or b in retired for a, kind, b in scene.relations)
    assert not validate(scene, True)
    scene.regenerate('/house', Building(design, '/land'))
    fresh, _ = loft_scene()
    assert semantic_digest(scene) == semantic_digest(fresh)
    assert encode(scene.nodes) == encode(fresh.nodes)
    export_scene(scene, work / 'regenerated.litematic')
    audit_pair(scene, work / 'regenerated.litematic')


@pytest.mark.parametrize('bays', (2, 3))
def test_veranda_has_intermediate_bearings_and_actual_entrance(bays):
    design = Blueprint(tuple(RoomSpec(x, 0, 0, 'living') for x in range(bays)), bay=11, porch_bays=bays, porch_depth=5)
    scene = lodge_scene(design, 20, garden=False)
    porch = scene.nodes['/building/porch']
    columns = porch.contract.decisions['columns']
    assert len(columns) >= 3 and max(b - a for a, b in zip(columns, columns[1:])) <= 12
    assert scene.view.port(porch.path, 'front').frame.origin[0] == 5
    q = porch.frame.point((columns[1], 2, -5))
    assert 'support' in {d.rule for d in validate(damaged(scene, q), True)}
    before = snapshot(scene)
    with pytest.raises(ValueError, match='contiguous front rooms'):
        scene.regenerate('/building', Building(replace(design, porch_bays=bays + 1), '/land'))
    assert snapshot(scene) == before


def test_habitable_roof_domains_are_explicit():
    with pytest.raises(ValueError, match='explicit roof policy'):
        Blueprint((RoomSpec(0, 0, 0, 'living'),), attic_purpose='bedroom').validate()
    with pytest.raises(ValueError, match='Attic purpose'):
        Blueprint((RoomSpec(0, 0, 0, 'living'),), attic_purpose='greenhouse').validate()
    scene = Scene(1, domain=Box((-2, -1, -2), (5, 20, 5)))
    scene.place('land', Terrain(scene.domain, constant(0)))
    before = snapshot(scene)
    with pytest.raises(ContractError, match='loft-light-support'):
        scene.place('unsupported-lantern', LoftLight(maximum_hanger=4), frame=Frame((1, 2, 1)))
    assert snapshot(scene) == before


def test_direct_recursive_inhabited_roof_and_policy_substitution():

    @dataclass(frozen=True)
    class TallerRoofPolicy:

        def heights(self, span):
            return RoofDesign((2,)).heights(span)

        def component(self, width, depth, axis, material):
            return ProfileRoof(width, depth, axis, material, RoofDesign((2,)))

    scene = Scene(33, domain=Box((-5, -3, -6), (18, 50, 18)))
    scene.place('land', Terrain(scene.domain, constant(0)))
    scene.place('room', Room('living', stair_up=True, upper_open=True), frame=Frame((0, 1, 0)), complete=False)
    port = scene.view.offers('/room', 'upper-floor-access')[0]
    before = snapshot(scene)
    with pytest.raises(ContractError, match='inhabited-roof-interface'):
        scene.place('roof', InhabitedRoof(11, 11, 'z', 'deepslate_tile'), frame=Frame(port.frame.origin, 1), bindings=(Binding('/room', port.key, 'connect'),))
    assert snapshot(scene) == before
    scene.place('roof', InhabitedRoof(11, 11, 'z', 'deepslate_tile'), frame=port.frame, bindings=(Binding('/room', port.key, 'connect'),))
    scene.finish_scope('/room')
    scene.finalize()
    first_height = max(p[1] for p in scene.cells('/roof', True))
    first_landing = scene.view.port('/roof', 'landing')
    scene.regenerate('/roof', InhabitedRoof(11, 11, 'z', 'deepslate_tile', TallerRoofPolicy()))
    assert max(p[1] for p in scene.cells('/roof', True)) > first_height
    assert scene.view.port('/roof', 'landing').frame == first_landing.frame
    assert not validate(scene, True)


def test_roof_aperture_metadata_cannot_hide_an_exterior_leak():
    scene, _ = loft_scene()
    node = scene.nodes['/house/roof-0/room']
    # Corrupt companion-style contract data in a fork; the validator checks the bound.
    broken = scene.fork()
    rules = tuple(replace(r, data={'apertures': ((-1, 3, -1),)}) if r.kind == 'enclosed-volume' else r for r in node.contract.rules)
    broken.nodes[node.path] = replace(node, contract=replace(node.contract, rules=rules))
    assert any(d.rule == 'enclosure-aperture' and d.component == node.path for d in validate(broken, True))


def test_sibling_roof_randomness_and_palette_free_principal_signature():
    from hearth.components import BoundaryPlanting
    scene = lodge_scene(roof_home(2), 2, garden=False)
    other = scene.fork()
    other.place('border', BoundaryPlanting('/building', '/land', density=.2))
    other.finalize()
    assert principal_signature(other) == principal_signature(scene)
    assert {p: c for p, c in other.blocks.items() if c.owner.startswith('/building')} == {p: c for p, c in scene.blocks.items() if c.owner.startswith('/building')}
    recolored = scene.fork()
    # Only color changes in a cloned actual scene; membership, geometry and role stay fixed.
    for p in scene.cells('/building', True):
        if name_of(scene.view.state(p)) == 'smooth_sandstone':
            recolored.blocks[p] = replace(scene.blocks[p], state=state_of('calcite'))
    assert principal_signature(recolored) == principal_signature(scene)


def test_canopy_contact_uses_flush_gable_glazing_instead_of_blank_infill():
    design = Blueprint((RoomSpec(0, 0, 0, 'living'),), bay=13, porch_depth=4, roof_axis='z', roof_design=RoofDesign((1, 1, 2)), attic_purpose='bedroom')
    scene = lodge_scene(design, 22, garden=False)
    window = scene.nodes['/building/roof-0/weather/cover/gable-front/window']
    assert window.type == 'window.flush'
    glass = scene.cells(window.path)
    assert len(glass) == 9 and all(name_of(scene.view.state(p)) == 'glass' for p in glass)
    assert scene.inspect(min(glass))['chain'][1]['type'] == 'wall.profile-gable'
    assert not validate(scene, True)
