# Actual ridge-lightwell geometry, authority, deterministic fitting and identity tests.
from dataclasses import replace
import pytest
from hearth import Scene, Box, Frame, Binding, ContractError
from hearth.kernel import Capability, Contract, Plan, validate
from hearth.blocks import AIR, name_of, state_of
from hearth.components import Blueprint, RoomSpec, Building, RidgeLantern
from hearth.components.roofcraft import RoofDesign, RoofCover
from hearth.components.primitives import Volume
from hearth.environment import Terrain, constant
from hearth.lodges import lodge_scene
from hearth.persistence import semantic_digest, export_scene, load_scene
from hearth.quality_assessment import audit_pair, principal_signature
from test.test_building_quality import damaged, snapshot


def design(length=7, bay=11, axis='x', hips=(0, 0)):
    return Blueprint((RoomSpec(0, 0, 0, 'living'), RoomSpec(1, 0, 0, 'kitchen')), bay=bay, roof_axis=axis, attic_purpose='library', porch_bays=2, roof_design=RoofDesign(end_hips=hips, ridge_component=RidgeLantern(length), ridge_required=True))


def bare_host(seed=0, length=23):
    scene = Scene(seed, domain=Box((-5, -3, -5), (35, 50, 35)))
    scene.place('land', Terrain(scene.domain, constant(0)))
    policy = RoofDesign(ridge_component=RidgeLantern(7), ridge_required=True)
    scene.place('host', RoofCover(11, length, 'deepslate_tile', policy, populate=False), frame=Frame((0, 3, 0)))
    port = scene.view.port('/host', 'ridge')
    return scene, port


@pytest.mark.parametrize('bay,length,axis,hips', ((9, 5, 'x', (0, 0)), (11, 7, 'x', (2, 2)), (13, 9, 'x', (3, 1)), (13, 5, 'z', (0, 2))))
def test_ridge_lightwell_keeps_actual_weather_enclosure_and_access(bay, length, axis, hips):
    scene = lodge_scene(design(length, bay, axis, hips), 4, 0, garden=False)
    assert not validate(scene, True)
    lanterns = [n for n in scene.nodes.values() if n.type == 'roof.ridge-lantern']
    assert lanterns
    for node in lanterns:
        opening = Box(*node.contract.decisions['well'])
        offer = scene.view.offers(node.path, 'daylight')[0]
        assert offer.region == node.frame.box(opening)
        assert not offer.facts['player_access']
        assert all(not scene.view.supports(node.frame.point(q)) for q in opening.cells())
        assert len([p for p in scene.cells(node.path, True) if name_of(scene.view.state(p)) == 'glass']) == 6 * length + (6 if length == 5 else 0)
        assert all(node.contract.envelope.contains(p) for p in scene.cells(node.path, True))
    assert any(n.type == 'room.loft.library' for n in scene.nodes.values())


@pytest.mark.parametrize('turn', range(4))
def test_lantern_rotation_windows_and_exact_reloaded_membership(turn, work):
    frame = Frame((35, 0, -20), turn)
    domain = frame.box(Box((-8, -3, -18), (38, 60, 28)))
    scene = Scene(7, domain=domain)
    scene.place('land', Terrain(domain, constant(2)))
    scene.place('building', Building(design(), '/land'), frame=frame)
    scene.finalize()
    lantern = next(n for n in scene.nodes.values() if n.type == 'roof.ridge-lantern')
    windows = [n for n in scene.nodes.values() if n.type == 'window.flush' and n.path.startswith(lantern.path)]
    assert len(windows) == 6 and len({n.path for n in windows}) == 6
    for window in windows:
        glass = min(scene.cells(window.path))
        chain = [n['id'] for n in scene.inspect(glass)['chain']]
        assert chain[:3] == [window.path, window.parent, lantern.path]
        assert chain[-1] == '/building'
    target = work / 'ridge windows.litematic'
    export_scene(scene, target)
    assert audit_pair(scene, target)['nodes'] == len(scene.nodes)
    restored = load_scene(target)
    assert restored.cells(lantern.path, True) == scene.cells(lantern.path, True)
    assert not validate(restored, True)


def test_missing_bearing_open_cap_broken_glazing_and_blocked_well_are_detected():
    scene = lodge_scene(design(), 1, 0, garden=False)
    node = next(n for n in scene.nodes.values() if n.type == 'roof.ridge-lantern')
    anchor = node.frame.point(node.contract.decisions['anchors'][0])
    assert 'support' in {d.rule for d in validate(damaged(scene, anchor), True)}
    cap = node.frame.point(node.contract.decisions['cap'][len(node.contract.decisions['cap']) // 2])
    assert 'enclosure' in {d.rule for d in validate(damaged(scene, cap), True)}
    glass = min(p for p in scene.cells(node.path, True) if name_of(scene.view.state(p)) == 'glass')
    assert 'enclosure' in {d.rule for d in validate(damaged(scene, glass), True)}
    # Corruption is deliberately outside the public generation API, as in the
    # existing negative tests. Block a real transmissive target with opaque rock.
    target = next(r.cells[1] for r in node.contract.rules if r.kind == 'daylight-path')
    assert 'daylight-path' in {d.rule for d in validate(damaged(scene, target, state_of('stone')), True)}


def test_misaligned_or_incompatible_ridge_bindings_roll_back_all_indexes():
    scene, port = bare_host()
    before = snapshot(scene)
    for frame, component in ((Frame(port.frame.origin, 1), RidgeLantern(7)), (port.frame, RidgeLantern(5))):
        with pytest.raises(ContractError, match='ridge-interface'):
            scene.place('bad', component, frame=frame, bindings=(Binding('/host', 'ridge'),))
        assert snapshot(scene) == before


def test_required_attachment_failure_optional_omission_and_permission_overflow():
    scene = Scene(3, domain=Box((-5, -3, -5), (30, 50, 30)))
    scene.place('land', Terrain(scene.domain, constant(0)))
    policy = RoofDesign(end_hips=(3, 3), ridge_component=RidgeLantern(9), ridge_required=True)
    before = snapshot(scene)
    with pytest.raises(ContractError, match='ridge-fit'):
        scene.place('roof', RoofCover(9, 9, 'deepslate_tile', policy), frame=Frame((0, 3, 0)))
    assert snapshot(scene) == before
    scene.place('roof', RoofCover(9, 9, 'deepslate_tile', replace(policy, ridge_required=False)), frame=Frame((0, 3, 0)))
    assert scene.view.contract('/roof').decisions['omitted_optional_ridge']
    scene, port = bare_host()
    before = snapshot(scene)

    outside = next(p for p in scene.cells('/host') if not port.region.contains(p))

    class Overflow:

        def capability(self):
            return Capability('test.ridge-overflow', ('ridge-attachment',))

        def negotiate(self, ctx, parameters):
            return Contract(Box((-5, -12, -10), (5, 8, 12)))

        def realize(self, ctx, contract):
            plan = Plan()
            plan.block(ctx.local(outside), AIR)
            return plan

    with pytest.raises(ContractError, match='write-authority|envelope'):
        scene.place('overflow', Overflow(), frame=port.frame, bindings=(Binding('/host', 'ridge'),))
    assert snapshot(scene) == before


def test_replace_remove_and_regenerate_restore_skin_without_stale_window_owners():
    scene, port = bare_host()
    before = semantic_digest(scene)
    scene.place('lightwell', RidgeLantern(7), frame=port.frame, bindings=(Binding('/host', 'ridge'),))
    current = scene.cells('/lightwell', True)
    windows = {n.path for n in scene.nodes.values() if n.path.startswith('/lightwell/')}
    replaced = set(scene.displaced['/lightwell'])
    assert replaced and not replaced.intersection(scene.cells('/host'))
    assert all(scene.view.owner(p) != '/host' for p in replaced)
    scene.remove('/lightwell')
    assert semantic_digest(scene) == before
    assert not windows.intersection(scene.nodes)
    assert not scene.cells('/lightwell', True)
    scene.place('lightwell', RidgeLantern(7), frame=port.frame, bindings=(Binding('/host', 'ridge'),))
    # The host observed the mounting volume before installation, so removal
    # invalidates that conservative spatial input. Replay both affected nodes.
    assert scene.nodes['/host'].stale
    scene.regenerate('/host', dependents=('/lightwell',), replacements={'/lightwell': RidgeLantern(7, timber='spruce')})
    assert current == scene.cells('/lightwell', True)
    assert all(p in scene.cells(scene.view.owner(p)) for p in current)
    assert not validate(scene, True)


def test_scoped_fit_replay_independent_branch_and_failed_candidate_do_not_change_mount():
    scene, port = bare_host(17)
    before = snapshot(scene)
    with pytest.raises(ContractError):
        scene.place('bad', RidgeLantern(5), frame=port.frame, bindings=(Binding('/host', 'ridge'),))
    assert snapshot(scene) == before
    scene.place('extra', Volume(Box((28, 1, 28), (28, 1, 28)), state_of('stone')))
    scene.place('lantern', RidgeLantern(7), frame=port.frame, bindings=(Binding('/host', 'ridge'),))
    other, expected = bare_host(17)
    other.place('lantern', RidgeLantern(7), frame=expected.frame, bindings=(Binding('/host', 'ridge'),))
    assert port == expected
    assert scene.cells('/lantern', True) == other.cells('/lantern', True)
    assert all(scene.inspect(p) == other.inspect(p) for p in scene.cells('/lantern', True))


def test_lightwell_changes_principal_roof_form_separately_from_contact():
    policy = design()
    first = lodge_scene(policy, 4, 0, garden=False)
    second = lodge_scene(replace(policy, roof_design=replace(policy.roof_design, ridge_component=None, ridge_required=False)), 4, 0, garden=False)
    assert principal_signature(first)[0] != principal_signature(second)[0]
    assert first.view.contract('/building').decisions['rooms'] == second.view.contract('/building').decisions['rooms']


def test_compatible_lower_dormers_fit_beside_high_lantern_overhang():
    scene, _ = bare_host(0)
    policy = RoofDesign(ridge_component=RidgeLantern(7), ridge_required=True)
    scene.regenerate('/host', RoofCover(11, 23, 'deepslate_tile', policy))
    assert sum(n.type == 'roof.dormer' for n in scene.nodes.values()) == 4
    assert sum(n.type == 'roof.ridge-lantern' for n in scene.nodes.values()) == 1
    assert not validate(scene, True)
