"""Relationship propagation across symmetric rails (ADR-001, wired).

A Dualpath pair is one physical dual-bay machine with a single controller PC
and a bay-selector switch, so BOTH bay records must carry the controls link to
that PC. Creating PC controls bayA where bayA has a Dualpath partner bayB
auto-creates PC controls bayB. The propagation set is M:N
(relationshiptypepropagations): controls propagates through Dualpath
(symmetric, consumed) and through partof (directional, declared but inert).
"""

from shopdb.extensions import db
from shopdb.core.models import Asset, AssetType
from shopdb.core.models.relationship import (
    RelationshipType,
    RelationshipTypePropagation,
    AssetRelationship,
)


def _make_asset(assetnumber, typeid):
    asset = Asset(assetnumber=assetnumber, assettypeid=typeid)
    db.session.add(asset)
    db.session.flush()
    return asset


def _setup_types():
    atype = AssetType(assettype='machine')
    db.session.add(atype)
    db.session.flush()

    controls = RelationshipType(relationshiptype='controls', isdirectional=True)
    dualpath = RelationshipType(relationshiptype='Dualpath', isdirectional=False)
    partof = RelationshipType(relationshiptype='partof', isdirectional=True)
    db.session.add_all([controls, dualpath, partof])
    db.session.flush()
    return atype, controls, dualpath, partof


def _rel_exists(sourceid, targetid, typeid):
    return AssetRelationship.query.filter_by(
        sourceassetid=sourceid, targetassetid=targetid, relationshiptypeid=typeid
    ).first() is not None


# ---------------------------------------------------------------------------
# Symmetric through-type propagates (controls fans across a Dualpath pair)
# ---------------------------------------------------------------------------

def test_controls_propagates_across_dualpath_pair(client, db, auth_headers):
    atype, controls, dualpath, partof = _setup_types()
    db.session.add(RelationshipTypePropagation(
        relationshiptypeid=controls.relationshiptypeid,
        throughtypeid=dualpath.relationshiptypeid))

    pc = _make_asset('PC-1', atype.assettypeid)
    baya = _make_asset('BAY-A', atype.assettypeid)
    bayb = _make_asset('BAY-B', atype.assettypeid)

    # the two bays are Dualpath partners (one physical machine)
    db.session.add(AssetRelationship(sourceassetid=baya.assetid, targetassetid=bayb.assetid,
                                     relationshiptypeid=dualpath.relationshiptypeid))
    db.session.commit()

    resp = client.post('/api/assets/relationships', headers=auth_headers, json={
        'sourceassetid': pc.assetid,
        'targetassetid': baya.assetid,
        'relationshiptypeid': controls.relationshiptypeid,
    })
    assert resp.status_code == 201, resp.get_json()
    assert resp.get_json()['data']['propagatedcount'] == 1

    # PC now controls BOTH bays
    assert _rel_exists(pc.assetid, baya.assetid, controls.relationshiptypeid)
    assert _rel_exists(pc.assetid, bayb.assetid, controls.relationshiptypeid)


def test_propagation_finds_partner_stored_either_direction(client, db, auth_headers):
    # Dualpath row stored partner->target; propagation must still catch it.
    atype, controls, dualpath, partof = _setup_types()
    db.session.add(RelationshipTypePropagation(
        relationshiptypeid=controls.relationshiptypeid,
        throughtypeid=dualpath.relationshiptypeid))

    pc = _make_asset('PC-2', atype.assettypeid)
    baya = _make_asset('BAY-A2', atype.assettypeid)
    bayb = _make_asset('BAY-B2', atype.assettypeid)
    # note: bayb is the SOURCE of the Dualpath row, target is baya
    db.session.add(AssetRelationship(sourceassetid=bayb.assetid, targetassetid=baya.assetid,
                                     relationshiptypeid=dualpath.relationshiptypeid))
    db.session.commit()

    resp = client.post('/api/assets/relationships', headers=auth_headers, json={
        'sourceassetid': pc.assetid,
        'targetassetid': baya.assetid,
        'relationshiptypeid': controls.relationshiptypeid,
    })
    assert resp.status_code == 201
    assert _rel_exists(pc.assetid, bayb.assetid, controls.relationshiptypeid)


def test_controls_propagates_when_bay_is_source(client, db, auth_headers):
    # Matches real dev data: controls is stored bay -> PC (source is the bay).
    # The bay's Dualpath partner must also get a controls -> PC row.
    atype, controls, dualpath, partof = _setup_types()
    db.session.add(RelationshipTypePropagation(
        relationshiptypeid=controls.relationshiptypeid,
        throughtypeid=dualpath.relationshiptypeid))

    pc = _make_asset('PC-S', atype.assettypeid)
    baya = _make_asset('BAY-AS', atype.assettypeid)
    bayb = _make_asset('BAY-BS', atype.assettypeid)
    db.session.add(AssetRelationship(sourceassetid=baya.assetid, targetassetid=bayb.assetid,
                                     relationshiptypeid=dualpath.relationshiptypeid))
    db.session.commit()

    resp = client.post('/api/assets/relationships', headers=auth_headers, json={
        'sourceassetid': baya.assetid,
        'targetassetid': pc.assetid,
        'relationshiptypeid': controls.relationshiptypeid,
    })
    assert resp.status_code == 201, resp.get_json()
    assert resp.get_json()['data']['propagatedcount'] == 1
    # partner bay now also controls the PC, same direction (bay -> PC)
    assert _rel_exists(bayb.assetid, pc.assetid, controls.relationshiptypeid)


# ---------------------------------------------------------------------------
# Idempotent: pre-existing propagated row is not duplicated
# ---------------------------------------------------------------------------

def test_propagation_idempotent(client, db, auth_headers):
    atype, controls, dualpath, partof = _setup_types()
    db.session.add(RelationshipTypePropagation(
        relationshiptypeid=controls.relationshiptypeid,
        throughtypeid=dualpath.relationshiptypeid))

    pc = _make_asset('PC-3', atype.assettypeid)
    baya = _make_asset('BAY-A3', atype.assettypeid)
    bayb = _make_asset('BAY-B3', atype.assettypeid)
    db.session.add(AssetRelationship(sourceassetid=baya.assetid, targetassetid=bayb.assetid,
                                     relationshiptypeid=dualpath.relationshiptypeid))
    # the mirrored row already exists
    db.session.add(AssetRelationship(sourceassetid=pc.assetid, targetassetid=bayb.assetid,
                                     relationshiptypeid=controls.relationshiptypeid))
    db.session.commit()

    resp = client.post('/api/assets/relationships', headers=auth_headers, json={
        'sourceassetid': pc.assetid,
        'targetassetid': baya.assetid,
        'relationshiptypeid': controls.relationshiptypeid,
    })
    assert resp.status_code == 201
    assert resp.get_json()['data']['propagatedcount'] == 0

    count = AssetRelationship.query.filter_by(
        sourceassetid=pc.assetid, targetassetid=bayb.assetid,
        relationshiptypeid=controls.relationshiptypeid).count()
    assert count == 1


# ---------------------------------------------------------------------------
# Non-propagating type creates no extras
# ---------------------------------------------------------------------------

def test_non_propagating_type_no_extras(client, db, auth_headers):
    atype, controls, dualpath, partof = _setup_types()
    # controls has NO propagation rows here

    pc = _make_asset('PC-4', atype.assettypeid)
    baya = _make_asset('BAY-A4', atype.assettypeid)
    bayb = _make_asset('BAY-B4', atype.assettypeid)
    db.session.add(AssetRelationship(sourceassetid=baya.assetid, targetassetid=bayb.assetid,
                                     relationshiptypeid=dualpath.relationshiptypeid))
    db.session.commit()

    resp = client.post('/api/assets/relationships', headers=auth_headers, json={
        'sourceassetid': pc.assetid,
        'targetassetid': baya.assetid,
        'relationshiptypeid': controls.relationshiptypeid,
    })
    assert resp.status_code == 201
    assert resp.get_json()['data']['propagatedcount'] == 0
    assert not _rel_exists(pc.assetid, bayb.assetid, controls.relationshiptypeid)


# ---------------------------------------------------------------------------
# Directional through-type (partof) does NOT propagate (deferred)
# ---------------------------------------------------------------------------

def test_directional_through_type_does_not_propagate(client, db, auth_headers):
    atype, controls, dualpath, partof = _setup_types()
    # controls propagates only through partof, which is directional -> inert
    db.session.add(RelationshipTypePropagation(
        relationshiptypeid=controls.relationshiptypeid,
        throughtypeid=partof.relationshiptypeid))

    pc = _make_asset('PC-5', atype.assettypeid)
    parent = _make_asset('PARENT-5', atype.assettypeid)
    child = _make_asset('CHILD-5', atype.assettypeid)
    db.session.add(AssetRelationship(sourceassetid=child.assetid, targetassetid=parent.assetid,
                                     relationshiptypeid=partof.relationshiptypeid))
    db.session.commit()

    resp = client.post('/api/assets/relationships', headers=auth_headers, json={
        'sourceassetid': pc.assetid,
        'targetassetid': parent.assetid,
        'relationshiptypeid': controls.relationshiptypeid,
    })
    assert resp.status_code == 201
    assert resp.get_json()['data']['propagatedcount'] == 0
    assert not _rel_exists(pc.assetid, child.assetid, controls.relationshiptypeid)


# ---------------------------------------------------------------------------
# CLI backfill creates missing rows and is idempotent run twice
# ---------------------------------------------------------------------------

def test_cli_backfill_propagates_and_is_idempotent(client, db, auth_headers, runner):
    atype, controls, dualpath, partof = _setup_types()
    db.session.add(RelationshipTypePropagation(
        relationshiptypeid=controls.relationshiptypeid,
        throughtypeid=dualpath.relationshiptypeid))

    pc = _make_asset('PC-6', atype.assettypeid)
    baya = _make_asset('BAY-A6', atype.assettypeid)
    bayb = _make_asset('BAY-B6', atype.assettypeid)
    db.session.add(AssetRelationship(sourceassetid=baya.assetid, targetassetid=bayb.assetid,
                                     relationshiptypeid=dualpath.relationshiptypeid))
    # legacy-import shape: controls exists only on the primary bay
    db.session.add(AssetRelationship(sourceassetid=pc.assetid, targetassetid=baya.assetid,
                                     relationshiptypeid=controls.relationshiptypeid))
    db.session.commit()

    assert not _rel_exists(pc.assetid, bayb.assetid, controls.relationshiptypeid)

    result = runner.invoke(args=['relationships', 'propagate'])
    assert result.exit_code == 0, result.output
    assert 'Propagated 1' in result.output
    assert _rel_exists(pc.assetid, bayb.assetid, controls.relationshiptypeid)

    # second run creates nothing more
    result2 = runner.invoke(args=['relationships', 'propagate'])
    assert result2.exit_code == 0, result2.output
    assert 'Propagated 0' in result2.output
    count = AssetRelationship.query.filter_by(
        sourceassetid=pc.assetid, targetassetid=bayb.assetid,
        relationshiptypeid=controls.relationshiptypeid).count()
    assert count == 1


# ---------------------------------------------------------------------------
# CLI fix-controls-direction flips reversed legacy rows to PC -> machine
# ---------------------------------------------------------------------------

def test_cli_fix_controls_direction(client, db, auth_headers, runner):
    atype, controls, dualpath, partof = _setup_types()
    ctype = AssetType(assettype='computer')
    db.session.add(ctype)
    db.session.flush()

    pc = _make_asset('PC-7', ctype.assettypeid)
    pc2 = _make_asset('PC-8', ctype.assettypeid)
    bay = _make_asset('BAY-7', atype.assettypeid)
    bay2 = _make_asset('BAY-8', atype.assettypeid)

    # reversed legacy row: machine -> PC
    db.session.add(AssetRelationship(sourceassetid=bay.assetid, targetassetid=pc.assetid,
                                     relationshiptypeid=controls.relationshiptypeid))
    # reversed row whose flip already exists -> reversed one gets deactivated
    db.session.add(AssetRelationship(sourceassetid=bay2.assetid, targetassetid=pc2.assetid,
                                     relationshiptypeid=controls.relationshiptypeid))
    db.session.add(AssetRelationship(sourceassetid=pc2.assetid, targetassetid=bay2.assetid,
                                     relationshiptypeid=controls.relationshiptypeid))
    db.session.commit()

    result = runner.invoke(args=['relationships', 'fix-controls-direction'])
    assert result.exit_code == 0, result.output
    assert 'Flipped 1' in result.output
    assert '1 reversed duplicate(s) deactivated' in result.output

    # first row now reads PC -> machine
    assert _rel_exists(pc.assetid, bay.assetid, controls.relationshiptypeid)
    assert not _rel_exists(bay.assetid, pc.assetid, controls.relationshiptypeid)
    # duplicate pair: canonical row stays active, reversed one deactivated
    rev = AssetRelationship.query.filter_by(
        sourceassetid=bay2.assetid, targetassetid=pc2.assetid,
        relationshiptypeid=controls.relationshiptypeid).first()
    assert rev is not None and rev.isactive is False

    # idempotent: second run finds nothing
    result2 = runner.invoke(args=['relationships', 'fix-controls-direction'])
    assert result2.exit_code == 0, result2.output
    assert 'Flipped 0' in result2.output
    assert '0 reversed duplicate(s) deactivated' in result2.output
