"""The landmark transform, and the wrong answer it must not give.

The case this exists for: a site's blueprint goes from 3300x2550 to 3308x4000
because a second level was added below the first. The existing floor is drawn at
the same scale - so the correct transform is identity, or identity plus an
offset. A transform derived from the ratio of image dimensions would scale Y by
4000/2550 = 1.5686 and be wrong for every marker on the level, while looking
like arithmetic somebody had thought about.

So the first test here is not that the transform works. It is that it does not
produce that particular plausible wrong answer.
"""

from datetime import datetime

import pytest

from shopdb.core.models import Asset, AssetType, Building, MapLevel
from shopdb.core.api.mappositions import derive_transform, solve_axis


# -- the arithmetic, with no app or database involved ------------------------

def test_two_landmarks_give_identity_when_the_drawing_did_not_move():
    """A canvas that grew taller with the old floor untouched: scale 1, no offset.

    This is the assertion that rules out the dimension-derived answer. Nothing
    about these landmarks mentions 2550 or 4000, and the result must not either.
    """
    transform, problem = derive_transform([
        {'fromx': 100, 'fromy': 100, 'tox': 100, 'toy': 100},
        {'fromx': 3000, 'fromy': 2000, 'tox': 3000, 'toy': 2000},
    ])
    assert problem is None
    assert transform['scalex'] == pytest.approx(1.0)
    assert transform['scaley'] == pytest.approx(1.0)
    assert transform['offsetx'] == pytest.approx(0.0)
    assert transform['offsety'] == pytest.approx(0.0)
    # The wrong answer, stated so a regression names itself.
    assert transform['scaley'] != pytest.approx(4000 / 2550, abs=0.01)


def test_a_level_added_above_gives_a_pure_y_offset():
    """Existing floor pushed down by 1450px: identity scale, offset 1450."""
    transform, problem = derive_transform([
        {'fromx': 100, 'fromy': 100, 'tox': 100, 'toy': 1550},
        {'fromx': 3000, 'fromy': 2000, 'tox': 3000, 'toy': 3450},
    ])
    assert problem is None
    assert transform['scaley'] == pytest.approx(1.0)
    assert transform['offsety'] == pytest.approx(1450.0)
    assert transform['scalex'] == pytest.approx(1.0)


def test_a_genuine_rescale_is_derived_per_axis():
    """X and Y can scale differently, which one uniform factor cannot express."""
    transform, _ = derive_transform([
        {'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 0},
        {'fromx': 1000, 'fromy': 1000, 'tox': 2000, 'toy': 1500},
    ])
    assert transform['scalex'] == pytest.approx(2.0)
    assert transform['scaley'] == pytest.approx(1.5)


def test_a_third_landmark_averages_measurement_error():
    """Points picked by eye carry a few pixels of error; more points cancel it
    rather than accumulating it."""
    transform, _ = derive_transform([
        {'fromx': 0, 'fromy': 0, 'tox': 2, 'toy': -2},
        {'fromx': 1000, 'fromy': 1000, 'tox': 999, 'toy': 1001},
        {'fromx': 2000, 'fromy': 2000, 'tox': 2001, 'toy': 1999},
    ])
    assert transform['scalex'] == pytest.approx(1.0, abs=0.01)
    assert transform['scaley'] == pytest.approx(1.0, abs=0.01)


def test_landmarks_on_one_column_are_refused_not_guessed():
    """Two points with the same X cannot determine a horizontal scale.

    Returning 1.0 here would look like success and silently leave X unscaled,
    which is the failure a user would discover after the write.
    """
    transform, problem = derive_transform([
        {'fromx': 500, 'fromy': 100, 'tox': 600, 'toy': 100},
        {'fromx': 500, 'fromy': 2000, 'tox': 600, 'toy': 2000},
    ])
    assert transform is None
    assert 'X' in problem


def test_one_landmark_is_not_enough():
    transform, problem = derive_transform([
        {'fromx': 1, 'fromy': 1, 'tox': 2, 'toy': 2}])
    assert transform is None
    assert 'two landmarks' in problem


def test_solve_axis_reports_an_undetermined_axis():
    assert solve_axis([(5, 9), (5, 11)]) is None
    assert solve_axis([(0, 0), (10, 20)]) == (2.0, 0.0)


# -- end to end, against the endpoints --------------------------------------

@pytest.fixture
def floor(db):
    """One building, two levels sized like the real before and after."""
    assettype = AssetType.query.filter_by(assettype='machine').first()
    if not assettype:
        assettype = AssetType(assettype='machine')
        db.session.add(assettype)
        db.session.flush()
    building = Building(buildingname='Main')
    db.session.add(building)
    db.session.flush()
    ground = MapLevel(buildingid=building.buildingid, levelname='Ground floor',
                      sortorder=0, isdefault=True, mapwidth=3300, mapheight=2550)
    second = MapLevel(buildingid=building.buildingid, levelname='Second floor',
                      sortorder=1, mapwidth=3308, mapheight=4000)
    db.session.add_all([ground, second])
    db.session.flush()
    assets = []
    for index, (x, y) in enumerate([(100, 100), (1500, 1200), (3200, 2500)]):
        asset = Asset(assetnumber='M%03d' % index, assettypeid=assettype.assettypeid,
                      mapx=x, mapy=y, levelid=ground.levelid,
                      mapverifiedat=datetime(2026, 1, 1))
        db.session.add(asset)
        assets.append(asset)
    db.session.commit()
    return {'ground': ground, 'second': second, 'assets': assets}


def _transform(client, headers, **body):
    return client.post('/api/mappositions/transform', json=body, headers=headers)


def test_a_dry_run_writes_nothing(client, db, floor, auth_headers):
    before = [(a.mapx, a.mapy, a.mapverifiedat) for a in floor['assets']]
    resp = _transform(client, auth_headers,
                      levelid=floor['ground'].levelid,
                      landmarks=[{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 1450},
                                 {'fromx': 1000, 'fromy': 1000, 'tox': 1000, 'toy': 2450}])
    assert resp.status_code == 200
    body = resp.get_json()['data']
    assert body['dryrun'] is True
    assert body['assetcount'] == 3
    db.session.expire_all()
    after = [(a.mapx, a.mapy, a.mapverifiedat)
             for a in Asset.query.order_by(Asset.assetid).all()]
    assert after == before


def test_applying_moves_markers_clears_review_and_snapshots(
        client, db, floor, auth_headers):
    resp = _transform(client, auth_headers,
                      levelid=floor['ground'].levelid, dryrun=False,
                      landmarks=[{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 1450},
                                 {'fromx': 1000, 'fromy': 1000, 'tox': 1000, 'toy': 2450}])
    assert resp.status_code == 200
    body = resp.get_json()['data']
    assert body['dryrun'] is False
    assert body['snapshotid']

    db.session.expire_all()
    moved = Asset.query.filter_by(assetnumber='M000').first()
    assert (moved.mapx, moved.mapy) == (100, 1550)
    # A transform is a guess, so the review state it had is gone.
    assert moved.mapverifiedat is None


def test_markers_pushed_off_the_canvas_are_counted(client, db, floor, auth_headers):
    """A marker at y=2500 offset by 1600 lands at 4100, past the 4000px canvas.

    Worth checking because a bad landmark pair is most likely to show up as
    markers leaving the drawing entirely, and a preview that does not count them
    lets that through.
    """
    resp = _transform(client, auth_headers,
                      levelid=floor['ground'].levelid,
                      tolevelid=floor['second'].levelid,
                      landmarks=[{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 1600},
                                 {'fromx': 1000, 'fromy': 1000, 'tox': 1000, 'toy': 2600}])
    body = resp.get_json()['data']
    assert body['outofboundscount'] == 1
    offender = next(m for m in body['moves'] if m['outofbounds'])
    assert offender['toy'] > floor['second'].mapheight


def test_restore_puts_positions_and_review_state_back(
        client, db, floor, auth_headers):
    original = {a.assetnumber: (a.mapx, a.mapy, a.mapverifiedat)
                for a in floor['assets']}
    applied = _transform(client, auth_headers,
                         levelid=floor['ground'].levelid, dryrun=False,
                         landmarks=[{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 1450},
                                    {'fromx': 1000, 'fromy': 1000, 'tox': 1000, 'toy': 2450}])
    snapshotid = applied.get_json()['data']['snapshotid']

    resp = client.post('/api/mappositions/snapshots/%d/restore' % snapshotid,
                       headers=auth_headers)
    assert resp.status_code == 200
    assert resp.get_json()['data']['restored'] == 3

    db.session.expire_all()
    for asset in Asset.query.all():
        assert (asset.mapx, asset.mapy, asset.mapverifiedat) == \
            original[asset.assetnumber]


def test_a_position_without_a_level_is_refused(client, db, floor, auth_headers):
    """The rule the whole model rests on: a position with no level cannot be
    stored, because it cannot be rendered."""
    resp = client.post('/api/mappositions/positions', json={
        'positions': [{'assetid': floor['assets'][0].assetid,
                       'mapx': 10, 'mapy': 20}]}, headers=auth_headers)
    assert resp.status_code == 400
    assert 'levelid' in resp.get_json()['data']['error']['message']


def test_placing_by_hand_counts_as_review(client, db, floor, auth_headers):
    asset = floor['assets'][0]
    client.post('/api/mappositions/verify',
                json={'assetids': [asset.assetid], 'unverify': True},
                headers=auth_headers)
    db.session.expire_all()
    assert Asset.query.get(asset.assetid).mapverifiedat is None

    resp = client.post('/api/mappositions/positions', json={
        'positions': [{'assetid': asset.assetid, 'mapx': 42, 'mapy': 43,
                       'levelid': floor['second'].levelid}]},
        headers=auth_headers)
    assert resp.status_code == 200
    db.session.expire_all()
    moved = Asset.query.get(asset.assetid)
    assert (moved.mapx, moved.mapy, moved.levelid) == (42, 43, floor['second'].levelid)
    assert moved.mapverifiedat is not None
