"""Correcting a misspelled search word without wrecking an identifier.

Pure arithmetic over a word list, so it is tested that way. The cases that
matter most are the ones where it must REFUSE: a wrong suggestion is acted on,
and here that means the wrong bay or the wrong cartridge.
"""

from shopdb.core.services.spellfix import (
    Vocabulary, damerau_levenshtein, max_distance,
)

SHOPFLOOR = [
    'Keyence vision system', 'Genspect', 'Telesis part marker',
    'wax-trace', 'Zebra ZT411', 'calibration', 'Haas Automation',
    'CSF16', 'CSF17', 'MT-1042', 'FC733SY3', 'printer', 'grinder',
]


def vocab():
    return Vocabulary(SHOPFLOOR)


# ------------------------------------------------------------------ distance

def test_a_transposition_costs_one_edit_not_two():
    """The commonest typing error. Plain Levenshtein charges two, which pushes
    a real typo past the threshold on a short word."""
    assert damerau_levenshtein('keyecne', 'keyence') == 1


def test_distance_allowed_scales_with_length():
    assert max_distance('bay') == 1
    assert max_distance('grinder') == 2
    assert max_distance('calibration') == 3


# ------------------------------------------------------------------ suggests

def test_it_corrects_vocabulary_no_dictionary_would_hold():
    assert vocab().suggest('keyecne') == 'keyence'
    assert vocab().suggest('genspct') == 'genspect'
    assert vocab().suggest('telisis') == 'telesis'


def test_a_word_that_is_already_right_is_left_alone():
    assert vocab().suggest('keyence') is None
    assert vocab().suggest('printer') is None


def test_a_word_nothing_resembles_gets_no_guess():
    """A wrong suggestion costs more than no suggestion."""
    assert vocab().suggest('helicopter') is None


# ------------------------------------------------------- identifier guards

def test_an_asset_number_is_never_corrected_to_a_different_one():
    """THE case this exists for. CSF16 to CSF17 is one edit and a different bay.

    Both are in the vocabulary, so without the digit guard the near miss is
    exactly the kind of thing edit distance loves to 'fix'.
    """
    assert vocab().suggest('csf18') is None
    assert vocab().suggest('mt-1043') is None


def test_a_typo_in_the_letters_of_an_identifier_still_corrects():
    """The guard is on the DIGITS, not on identifiers as a whole: the digits
    have to match, and here they do."""
    assert vocab().suggest('cfs16') == 'csf16'


def test_the_first_character_must_match():
    """Typos land in the middle and at the end. Requiring the first key costs
    little recall and removes most of the nonsense."""
    assert vocab().suggest('teyence') is None


def test_a_tie_produces_no_suggestion():
    """Two equally good answers means there is no answer. CSF16 and CSF17 are
    both one edit from CSF1x, and offering either implies a confidence that is
    not there - but the digit guard rejects both first, so build a case where
    only the tie is in play."""
    tied = Vocabulary(['barn', 'bard'])
    assert tied.suggest('bare') is None


# ---------------------------------------------------------------- queries

def test_a_multi_word_query_corrects_only_what_is_wrong():
    assert vocab().suggest_query('keyecne calibration') == 'keyence calibration'


def test_a_query_that_is_entirely_fine_returns_none():
    assert vocab().suggest_query('keyence calibration') is None


def test_short_words_are_left_alone():
    """Below three characters there is not enough word to correct."""
    assert vocab().suggest('cs') is None
    assert vocab().suggest_query('cs') is None


def test_the_vocabulary_comes_from_the_data():
    """A vendor added this morning is correctable this morning."""
    fresh = vocab()
    assert fresh.suggest('renishw') is None
    fresh.add('Renishaw')
    assert fresh.suggest('renishw') == 'renishaw'


# ------------------------------------------------------- wired into search

def test_a_search_that_finds_nothing_offers_a_spelling(client, db, auth_headers):
    """End to end: the vocabulary is harvested from real rows, and a misspelled
    query that returns no results comes back with a better spelling."""
    from shopdb.core.models import Vendor
    from shopdb.extensions import cache

    db.session.add(Vendor(vendor='Keyence'))
    db.session.commit()
    cache.delete('search_vocabulary')

    resp = client.get('/api/search?q=keyecne', headers=auth_headers)
    assert resp.status_code == 200
    data = resp.get_json()['data']
    assert data['results'] == []
    assert data.get('suggestion') == 'keyence'


def test_a_search_that_finds_something_offers_nothing(client, db, auth_headers):
    from shopdb.core.models import Vendor
    from shopdb.extensions import cache

    db.session.add(Vendor(vendor='Keyence'))
    db.session.commit()
    cache.delete('search_vocabulary')

    resp = client.get('/api/search?q=keyence', headers=auth_headers)
    assert 'suggestion' not in resp.get_json()['data']


def test_a_mistyped_asset_number_is_not_corrected_to_a_real_one(
        client, db, auth_headers):
    """The guard, through the endpoint. CSF17 exists; CSF18 does not; and the
    answer to "CSF18" must be no results and NO suggestion, not a different
    bay."""
    from shopdb.core.models import Asset, AssetType
    from shopdb.extensions import cache

    assettype = AssetType.query.first() or AssetType(assettype='machine')
    if not assettype.assettypeid:
        db.session.add(assettype)
        db.session.flush()
    db.session.add(Asset(assetnumber='CSF17', assettypeid=assettype.assettypeid))
    db.session.commit()
    cache.delete('search_vocabulary')

    resp = client.get('/api/search?q=CSF18', headers=auth_headers)
    data = resp.get_json()['data']
    assert data['results'] == []
    assert 'suggestion' not in data


def test_the_suggest_endpoint_serves_any_page(client, db, auth_headers):
    """The shared endpoint every list uses, rather than a suggestion key added
    to thirty search routes."""
    from shopdb.core.models import Vendor
    from shopdb.extensions import cache

    db.session.add(Vendor(vendor='Keyence'))
    db.session.commit()
    cache.delete('search_vocabulary')

    resp = client.get('/api/search/suggest?q=keyecne', headers=auth_headers)
    assert resp.status_code == 200
    assert resp.get_json()['data']['suggestion'] == 'keyence'


def test_the_suggest_endpoint_says_null_rather_than_guessing(client, db,
                                                             auth_headers):
    from shopdb.extensions import cache
    cache.delete('search_vocabulary')
    resp = client.get('/api/search/suggest?q=helicopter', headers=auth_headers)
    assert resp.get_json()['data']['suggestion'] is None


def test_the_suggest_endpoint_ignores_a_too_short_query(client, auth_headers):
    resp = client.get('/api/search/suggest?q=c', headers=auth_headers)
    assert resp.status_code == 200
    assert resp.get_json()['data']['suggestion'] is None
