"""Testy budowania bloku <user_selection> — wiele kliknięć, wiele ekranów, momenty wskazania."""

from __future__ import annotations

import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from lib import user_selection                                              # noqa: E402
from lib.user_selection import (                                            # noqa: E402
    MARKER_HTML,
    RECORD_BTN_XPATH,
    SCROLLED_XPATH,
    UserSelectionMismatchError,
    build_user_selection_block,
    marker_html,
)

VIEW_A = (
    '<div id="root">'
    '<p id="first">jeden</p>'
    '<p id="second">dwa</p>'
    '<span>trzy</span>'
    '</div>'
)
VIEW_B = (
    '<div id="root-b">'
    '<p id="other">inny ekran</p>'
    '</div>'
)

HASH_A = "aaa111"
HASH_B = "bbb222"


@pytest.fixture(autouse=True)
def stored_views(monkeypatch):
    """Podstawia magazyn widoków, żeby testy nie zależały od stanu last_view."""
    store = {HASH_A: VIEW_A, HASH_B: VIEW_B}
    monkeypatch.setattr(user_selection.last_view, "get_html", store.get)
    monkeypatch.setattr(user_selection.last_view, "get_last_html", lambda: VIEW_A)


# ------------------------------------------------------------------ zaznaczanie

def test_single_xpath_marks_one_element():
    block = build_user_selection_block('//*[@id="first"]')

    assert block.count(MARKER_HTML) == 1
    assert "zaznaczył ten element" in block
    assert block.index(MARKER_HTML) < block.index('id="first"')


def test_multiple_xpaths_mark_every_element():
    block = build_user_selection_block(['//*[@id="first"]', '//*[@id="second"]'])

    assert block.count(MARKER_HTML) == 2
    assert "zaznaczył te elementy" in block
    for anchor in ('id="first"', 'id="second"'):
        assert block.index(MARKER_HTML) < block.index(anchor)


def test_record_button_xpath_is_ignored():
    block = build_user_selection_block([RECORD_BTN_XPATH])

    assert MARKER_HTML not in block
    assert "niczego w nim nie zaznaczył" in block


def test_record_button_mixed_with_real_selection():
    block = build_user_selection_block([RECORD_BTN_XPATH, '//*[@id="first"]'])

    assert block.count(MARKER_HTML) == 1
    assert "zaznaczył ten element" in block


def test_empty_input_gives_plain_view():
    for value in (None, [], "", [""]):
        block = build_user_selection_block(value)
        assert "niczego w nim nie zaznaczył" in block, value


def test_unmatched_xpath_raises():
    with pytest.raises(UserSelectionMismatchError):
        build_user_selection_block('//*[@id="nie-ma-takiego"]')


def test_one_bad_xpath_aborts_whole_selection():
    """Niedopasowanie ma przerwać przed modyfikacją drzewa, nie zostawiać połowy znaczników."""
    with pytest.raises(UserSelectionMismatchError):
        build_user_selection_block(['//*[@id="first"]', '//*[@id="nie-ma-takiego"]'])


def test_invalid_xpath_raises():
    with pytest.raises(UserSelectionMismatchError):
        build_user_selection_block("//[[[")


# ------------------------------------------------------- momenty wskazania

def test_time_lands_in_marker():
    block = build_user_selection_block('//*[@id="first"]', xpath_times=["1.2"])

    assert marker_html(1.2) in block
    assert "sekundach od początku nagrania" in block


def test_each_click_keeps_its_own_time():
    block = build_user_selection_block(
        ['//*[@id="first"]', '//*[@id="second"]'],
        xpath_times=["1.2", "3.0"],
    )

    assert block.index(marker_html(1.2)) < block.index('id="first"')
    assert block.index(marker_html(3.0)) < block.index('id="second"')


def test_repeated_click_is_a_separate_timed_marker():
    """Powrót do tego samego elementu to informacja, nie duplikat — i to w kolejności klikania."""
    block = build_user_selection_block(
        ['//*[@id="first"]', '//*[@id="second"]', '//*[@id="first"]'],
        xpath_times=["1.2", "3.0", "7.4"],
    )

    assert block.index(marker_html(1.2)) < block.index(marker_html(7.4)) < block.index('id="first"')
    assert block.count(MARKER_HTML) == 0     # każdy znacznik ma swój moment


def test_time_is_rounded_to_one_decimal():
    block = build_user_selection_block('//*[@id="first"]', xpath_times=["3.456"])

    assert marker_html(3.5) in block


def test_root_selection_keeps_its_time():
    block = build_user_selection_block(".", xpath_times=["2.5"])

    assert block.index(marker_html(2.5)) < block.index('id="root"')


def test_missing_times_fall_back_to_plain_marker():
    block = build_user_selection_block('//*[@id="first"]')

    assert MARKER_HTML in block
    assert "sekundach od początku nagrania" not in block


def test_unreadable_time_drops_only_its_own_marker():
    block = build_user_selection_block(
        ['//*[@id="first"]', '//*[@id="second"]'],
        xpath_times=["nie-liczba", "3.0"],
    )

    assert block.index(MARKER_HTML) < block.index('id="first"')
    assert block.index(marker_html(3.0)) < block.index('id="second"')


def test_time_count_mismatch_drops_all_times():
    """Rozjazd długości list mógłby przypiąć czas do złego elementu — lepiej bez czasów."""
    block = build_user_selection_block(
        ['//*[@id="first"]', '//*[@id="second"]'],
        xpath_times=["1.2"],
    )

    assert block.count(MARKER_HTML) == 2
    assert "sekundach od początku nagrania" not in block


def test_record_button_does_not_shift_metadata():
    """Odfiltrowanie kulki nie może przesunąć hashy ani czasów na sąsiednie pozycje."""
    block = build_user_selection_block(
        [RECORD_BTN_XPATH, '//*[@id="other"]'],
        xpath_hashes=[HASH_A, HASH_B],
        xpath_times=["0.5", "2.0"],
    )

    assert f'<user_selection hash="{HASH_B}">' in block
    assert marker_html(2.0) in block
    assert marker_html(0.5) not in block


# ------------------------------------------------------------- wiele ekranów

def test_clicks_on_two_screens_give_two_blocks():
    block = build_user_selection_block(
        ['//*[@id="first"]', '//*[@id="other"]'],
        xpath_hashes=[HASH_A, HASH_B],
        xpath_times=["1.0", "5.0"],
    )

    assert f'<user_selection hash="{HASH_A}">' in block
    assert f'<user_selection hash="{HASH_B}">' in block
    assert "na kilku ekranach" in block
    assert block.count("</user_selection>") == 2


def test_screen_blocks_follow_first_click_order():
    block = build_user_selection_block(
        ['//*[@id="other"]', '//*[@id="first"]'],
        xpath_hashes=[HASH_B, HASH_A],
        xpath_times=["1.0", "2.0"],
    )

    assert block.index(f'hash="{HASH_B}"') < block.index(f'hash="{HASH_A}"')


def test_same_xpath_on_two_screens_is_two_selections():
    """Ten sam xpath na dwóch ekranach to dwa różne elementy, nie powtórka."""
    block = build_user_selection_block(
        ['//*[@id="root"]/p[1]', '//*[@id="root-b"]/p[1]'],
        xpath_hashes=[HASH_A, HASH_B],
    )

    assert block.count("<user_selection") == 2
    assert block.count(MARKER_HTML) == 2


def test_clicks_group_per_screen():
    block = build_user_selection_block(
        ['//*[@id="first"]', '//*[@id="other"]', '//*[@id="second"]'],
        xpath_hashes=[HASH_A, HASH_B, HASH_A],
        xpath_times=["1.0", "2.0", "3.0"],
    )

    screen_a = block.split(f'<user_selection hash="{HASH_A}">')[1].split("</user_selection>")[0]
    assert marker_html(1.0) in screen_a and marker_html(3.0) in screen_a
    assert marker_html(2.0) not in screen_a


def test_unknown_screen_hash_raises():
    """Nieznany ekran nie może spaść na inny widok — znaczniki poszłyby w złe drzewo."""
    with pytest.raises(UserSelectionMismatchError):
        build_user_selection_block(
            ['//*[@id="first"]'],
            xpath_hashes=["nie-ma-takiego-ekranu"],
        )


def test_missing_screen_hashes_fall_back_to_shared_hash():
    """Starsza strona nie wysyła xpath_hash — wszystko należy do ekranu z pola hash."""
    block = build_user_selection_block(['//*[@id="other"]'], page_hash=HASH_B)

    assert f'<user_selection hash="{HASH_B}">' in block
    assert block.count("<user_selection") == 1


def test_blank_screen_hash_falls_back_to_shared_hash():
    """Puste xpath_hash (element wskazany, gdy karuzela nie znała hasha) idzie do wspólnego ekranu."""
    block = build_user_selection_block(
        ['//*[@id="other"]'],
        page_hash=HASH_B,
        xpath_hashes=[""],
    )

    assert f'<user_selection hash="{HASH_B}"' in block


# --------------------------------------------------- ścieżka ekranów (przewijanie)

def test_visited_screen_gives_plain_view_with_its_hash():
    """Ekran, przez który przewinięto: treść jako kontekst i hash jako cel podmianki."""
    block = build_user_selection_block(
        [SCROLLED_XPATH],
        xpath_hashes=[HASH_B],
        xpath_times=["0.0"],
    )

    assert f'<user_view hash="{HASH_B}"' in block
    assert 'seen_at="0.0s"' in block
    assert "niczego w nim nie zaznaczył" in block
    assert MARKER_HTML not in block


def test_scrolled_sentinel_is_not_evaluated_as_xpath():
    """Sentinel nie może trafić do ewaluatora xpath — inaczej wywaliłby całe polecenie."""
    block = build_user_selection_block([SCROLLED_XPATH], xpath_hashes=[HASH_A])

    assert "trzy" in block            # treść ekranu jest w bloku
    assert MARKER_HTML not in block


def test_visit_and_click_split_into_two_kinds_of_block():
    block = build_user_selection_block(
        [SCROLLED_XPATH, '//*[@id="first"]', SCROLLED_XPATH],
        xpath_hashes=[HASH_A, HASH_A, HASH_B],
        xpath_times=["0.0", "1.5", "4.0"],
    )

    assert f'<user_selection hash="{HASH_A}"' in block
    assert f'<user_view hash="{HASH_B}"' in block
    assert 'seen_at="4.0s"' in block
    assert marker_html(1.5) in block
    assert "przechodził między ekranami" in block


def test_screen_blocks_follow_visit_order():
    block = build_user_selection_block(
        [SCROLLED_XPATH, SCROLLED_XPATH],
        xpath_hashes=[HASH_B, HASH_A],
        xpath_times=["0.0", "2.0"],
    )

    assert block.index(f'hash="{HASH_B}"') < block.index(f'hash="{HASH_A}"')


def test_revisited_screen_keeps_both_moments():
    """Powrót na ekran to informacja — oba momenty lecą w seen_at tego samego bloku."""
    block = build_user_selection_block(
        [SCROLLED_XPATH, SCROLLED_XPATH, SCROLLED_XPATH],
        xpath_hashes=[HASH_A, HASH_B, HASH_A],
        xpath_times=["0.0", "2.0", "5.0"],
    )

    assert 'seen_at="0.0s, 5.0s"' in block
    assert block.count("<user_view hash=") == 2      # nagłówek też wymienia nazwę tagu


def test_visited_screen_without_stored_view_is_reported_not_fatal():
    """Ekranu tylko przewiniętego nie ma czym zastąpić, ale i nie ma czego zepsuć."""
    block = build_user_selection_block(
        [SCROLLED_XPATH, SCROLLED_XPATH],
        xpath_hashes=[HASH_A, "juz-go-nie-ma"],
        xpath_times=["0.0", "3.0"],
    )

    assert f'hash="{HASH_A}"' in block
    assert "juz-go-nie-ma" in block
    assert "pominięte" in block


def test_all_visited_screens_missing_leaves_only_the_note():
    block = build_user_selection_block(
        [SCROLLED_XPATH],
        xpath_hashes=["juz-go-nie-ma"],
        xpath_times=["0.0"],
    )

    assert "pominięte" in block
    assert "<user_view" not in block


def test_click_on_missing_screen_still_raises():
    """Pominięcie wolno tylko odwiedzinom; wskazanie w złym drzewie myli agenta."""
    with pytest.raises(UserSelectionMismatchError):
        build_user_selection_block(
            [SCROLLED_XPATH, '//*[@id="first"]'],
            xpath_hashes=[HASH_A, "juz-go-nie-ma"],
        )


# ------------------------------------------------------------- hash i podmianki

def test_no_selection_exposes_hash_of_shown_view(monkeypatch):
    """Bez zaznaczeń agent też musi dostać identyfikator ekranu, żeby móc go zmienić."""
    monkeypatch.setattr(user_selection.last_view, "get_last_hash", lambda: HASH_A)

    block = build_user_selection_block(None)

    assert f'<user_view hash="{HASH_A}">' in block


def test_replaced_screen_points_at_its_successor(monkeypatch):
    """Widok podmieniony po nagraniu: agent ma zmieniać ten, który użytkownik ma teraz."""
    monkeypatch.setattr(
        user_selection.last_view,
        "replaced_by",
        lambda page_hash: "nowy999" if page_hash == HASH_A else None,
    )

    block = build_user_selection_block(
        ['//*[@id="first"]'],
        xpath_hashes=[HASH_A],
    )

    assert 'replaced_by="nowy999"' in block
    assert "został już podmieniony" in block
