"""
Step 21 — JSON Import: Title-Conflict Resolution UX
- test_same_user_title_conflict_shown_as_collision: title-only conflict (different id) surfaces
  in the collisions screen instead of being silently dropped
- test_same_user_title_conflict_skip: skip leaves the existing exercise untouched, no duplicate row
- test_same_user_title_conflict_replace: replace updates the existing row in place (keeps its id)
- test_same_user_title_conflict_dependency_redirect_skip: a combo in the same batch referencing the
  conflicting exercise's imported id ends up pointing at the existing exercise's id after skip
- test_same_user_title_conflict_dependency_redirect_replace: same redirect check, replace branch
- test_cross_user_title_conflict_skip: suffixed-title conflict (re-import from same external user)
  offers Replace/Skip instead of a forced rename; skip leaves the previous import untouched
- test_cross_user_title_conflict_replace: same scenario, replace updates the previous import in place
"""

import json
from playwright.sync_api import Page, expect


def _wait_for_db(page: Page, app_url: str):
    page.goto(app_url)
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )


def _go_to_exercises(page: Page, app_url: str):
    _wait_for_db(page, app_url)
    page.click('a[href="#/exercises"]')
    page.wait_for_timeout(500)


def _clean_all(page: Page):
    page.evaluate(
        """async () => {
            const combos = await window.__repos.combos.getAll();
            for (const c of combos) await window.__repos.combos.delete(c.id);
            const exercises = await window.__repos.exercises.getAll();
            for (const ex of exercises) await window.__repos.exercises.delete(ex.id);
            const suffixes = await window.__repos.suffixes.getAll();
            for (const s of suffixes) await window.__repos.suffixes.delete(s.user_uuid);
        }"""
    )


def _get_local_uuid(page: Page):
    return page.evaluate("async () => await window.__repos.settings.get('user_uuid')")


def _create_exercise_directly(page: Page, title, created_by=None, exercise_id=None):
    return page.evaluate(
        """async ([title, createdBy, exerciseId]) => {
            const uuid = await window.__repos.settings.get('user_uuid');
            const cb = createdBy || uuid;
            if (exerciseId) {
                await window.__repos.exercises.createWithId(exerciseId, {
                    title, description: '', asymmetric: false, alternate: false,
                    image_urls: [], video_urls: [], default_reps: 10, default_weight: 20,
                    created_by: cb,
                });
                return exerciseId;
            }
            return await window.__repos.exercises.create({
                title, description: '', asymmetric: false, alternate: false,
                image_urls: [], video_urls: [], default_reps: 10, default_weight: 20,
                created_by: cb,
            });
        }""",
        [title, created_by, exercise_id],
    )


def _build_export_json(data, exporter_name, exporter_uuid):
    envelope = {
        "metadata": {
            "appName": "trainus",
            "appVersion": "1.0.0",
            "schemaVersion": 1,
            "exportDate": "2026-06-16T12:00:00.000Z",
            "exportedBy": {"name": exporter_name, "uuid": exporter_uuid},
        },
        "data": data,
    }
    return json.dumps(envelope)


def _trigger_import_with_json(page: Page, json_str: str, test_id: str = "exercise-import-btn"):
    with page.expect_file_chooser() as fc_info:
        page.click(f'[data-testid="{test_id}"]')
    file_chooser = fc_info.value
    file_chooser.set_files(
        [
            {
                "name": "test-import.json",
                "mimeType": "application/json",
                "buffer": json_str.encode("utf-8"),
            }
        ]
    )


def _exercise_count_by_title(page: Page, title: str) -> int:
    return page.evaluate(
        """async (title) => {
            const all = await window.__repos.exercises.getAll();
            return all.filter((e) => e.title === title).length;
        }""",
        title,
    )


# ============================================================
# Same-user title conflicts (different id, same title)
# ============================================================


def test_same_user_title_conflict_shown_as_collision(page: Page, app_url: str):
    """A same-user import whose title matches an existing exercise (different id) surfaces
    in the collisions screen instead of being silently dropped or raising a raw DB error."""
    _go_to_exercises(page, app_url)
    _clean_all(page)

    local_uuid = _get_local_uuid(page)
    _create_exercise_directly(page, "Push-ups")

    imported = {
        "id": "11111111-1111-1111-1111-111111111111",
        "title": "Push-ups",
        "description": "Imported version",
        "asymmetric": False,
        "alternate": False,
        "image_urls": [],
        "video_urls": [],
        "default_reps": 99,
        "default_weight": 99,
        "created_by": local_uuid,
    }
    json_str = _build_export_json({"exercises": [imported]}, "Local User", local_uuid)

    _trigger_import_with_json(page, json_str)

    expect(page.locator('[data-testid="import-collisions-step"]')).to_be_visible(timeout=5000)
    expect(page.locator('[data-testid="collision-item-exercise-0"]')).to_contain_text("Push-ups")

    page.click('[data-testid="import-cancel"]')
    _clean_all(page)


def test_same_user_title_conflict_skip(page: Page, app_url: str):
    """Skip on a same-user title conflict leaves the existing exercise untouched and creates
    no duplicate row under the imported id."""
    _go_to_exercises(page, app_url)
    _clean_all(page)

    local_uuid = _get_local_uuid(page)
    existing_id = _create_exercise_directly(page, "Push-ups")
    new_id = "22222222-2222-2222-2222-222222222222"

    imported = {
        "id": new_id,
        "title": "Push-ups",
        "description": "Imported version",
        "asymmetric": False,
        "alternate": False,
        "image_urls": [],
        "video_urls": [],
        "default_reps": 99,
        "default_weight": 99,
        "created_by": local_uuid,
    }
    json_str = _build_export_json({"exercises": [imported]}, "Local User", local_uuid)

    _trigger_import_with_json(page, json_str)
    expect(page.locator('[data-testid="import-collisions-step"]')).to_be_visible(timeout=5000)

    # Default action is Skip
    page.click('[data-testid="import-apply"]')
    expect(page.locator('[data-testid="import-report-step"]')).to_be_visible(timeout=5000)
    expect(page.locator('[data-testid="import-report-skipped"]')).to_be_visible()
    page.click('[data-testid="import-done"]')

    existing = page.evaluate(
        f"""async () => await window.__repos.exercises.getById('{existing_id}')"""
    )
    assert existing["default_reps"] == 10, "Existing exercise must be unchanged"

    duplicate = page.evaluate(f"""async () => await window.__repos.exercises.getById('{new_id}')""")
    assert duplicate is None, "No new row should be created under the imported id"

    assert _exercise_count_by_title(page, "Push-ups") == 1
    _clean_all(page)


def test_same_user_title_conflict_replace(page: Page, app_url: str):
    """Replace on a same-user title conflict updates the existing row in place (keeps its id)."""
    _go_to_exercises(page, app_url)
    _clean_all(page)

    local_uuid = _get_local_uuid(page)
    existing_id = _create_exercise_directly(page, "Push-ups")
    new_id = "33333333-3333-3333-3333-333333333333"

    imported = {
        "id": new_id,
        "title": "Push-ups",
        "description": "Imported version",
        "asymmetric": False,
        "alternate": False,
        "image_urls": [],
        "video_urls": [],
        "default_reps": 99,
        "default_weight": 99,
        "created_by": local_uuid,
    }
    json_str = _build_export_json({"exercises": [imported]}, "Local User", local_uuid)

    _trigger_import_with_json(page, json_str)
    expect(page.locator('[data-testid="import-collisions-step"]')).to_be_visible(timeout=5000)

    page.click('[data-testid="collision-replace-exercise-0"]')
    page.click('[data-testid="import-apply"]')
    expect(page.locator('[data-testid="import-report-step"]')).to_be_visible(timeout=5000)
    expect(page.locator('[data-testid="import-report-exercises-replaced"]')).to_be_visible()
    page.click('[data-testid="import-done"]')

    existing = page.evaluate(
        f"""async () => await window.__repos.exercises.getById('{existing_id}')"""
    )
    assert existing is not None, "Existing row must keep its original id"
    assert existing["default_reps"] == 99, "Existing row must be updated with imported data"

    duplicate = page.evaluate(f"""async () => await window.__repos.exercises.getById('{new_id}')""")
    assert duplicate is None, "No new row should be created under the imported id"

    assert _exercise_count_by_title(page, "Push-ups") == 1
    _clean_all(page)


# ============================================================
# Same-user title conflict + same-batch dependency redirect
# ============================================================


def _build_conflict_with_combo_json(local_uuid, new_exercise_id, combo_id):
    imported_exercise = {
        "id": new_exercise_id,
        "title": "Push-ups",
        "description": "Imported version",
        "asymmetric": False,
        "alternate": False,
        "image_urls": [],
        "video_urls": [],
        "default_reps": 99,
        "default_weight": 99,
        "created_by": local_uuid,
    }
    imported_combo = {
        "id": combo_id,
        "title": "Conflict Combo",
        "description": "",
        "type": "NONE",
        "timer_config": {},
        "created_by": local_uuid,
        "exercises": [
            {
                "position": 0,
                "exercise_id": new_exercise_id,
                "default_reps": 5,
                "default_weight": 10,
                "title": "Push-ups",
                "asymmetric": False,
                "alternate": False,
            }
        ],
    }
    return _build_export_json(
        {"exercises": [imported_exercise], "combos": [imported_combo]}, "Local User", local_uuid
    )


def test_same_user_title_conflict_dependency_redirect_skip(page: Page, app_url: str):
    """A combo in the same import batch referencing the conflicting exercise's imported id
    ends up pointing at the existing exercise's id once the conflict is skipped."""
    _go_to_exercises(page, app_url)
    _clean_all(page)

    local_uuid = _get_local_uuid(page)
    existing_id = _create_exercise_directly(page, "Push-ups")
    new_exercise_id = "44444444-4444-4444-4444-444444444444"
    combo_id = "55555555-5555-5555-5555-555555555555"

    json_str = _build_conflict_with_combo_json(local_uuid, new_exercise_id, combo_id)
    _trigger_import_with_json(page, json_str)
    expect(page.locator('[data-testid="import-collisions-step"]')).to_be_visible(timeout=5000)

    # Default action is Skip
    page.click('[data-testid="import-apply"]')
    expect(page.locator('[data-testid="import-report-step"]')).to_be_visible(timeout=10000)
    page.click('[data-testid="import-done"]')

    combo = page.evaluate(f"""async () => await window.__repos.combos.getById('{combo_id}')""")
    assert combo is not None
    assert len(combo["exercises"]) == 1
    assert combo["exercises"][0]["exercise_id"] == existing_id, (
        "Combo must reference the existing exercise's id, not the skipped imported id"
    )
    assert _exercise_count_by_title(page, "Push-ups") == 1
    _clean_all(page)


def test_same_user_title_conflict_dependency_redirect_replace(page: Page, app_url: str):
    """Same dependency-redirect check, this time resolving the conflict with Replace."""
    _go_to_exercises(page, app_url)
    _clean_all(page)

    local_uuid = _get_local_uuid(page)
    existing_id = _create_exercise_directly(page, "Push-ups")
    new_exercise_id = "66666666-6666-6666-6666-666666666666"
    combo_id = "77777777-7777-7777-7777-777777777777"

    json_str = _build_conflict_with_combo_json(local_uuid, new_exercise_id, combo_id)
    _trigger_import_with_json(page, json_str)
    expect(page.locator('[data-testid="import-collisions-step"]')).to_be_visible(timeout=5000)

    page.click('[data-testid="collision-replace-exercise-0"]')
    page.click('[data-testid="import-apply"]')
    expect(page.locator('[data-testid="import-report-step"]')).to_be_visible(timeout=10000)
    page.click('[data-testid="import-done"]')

    combo = page.evaluate(f"""async () => await window.__repos.combos.getById('{combo_id}')""")
    assert combo is not None
    assert len(combo["exercises"]) == 1
    assert combo["exercises"][0]["exercise_id"] == existing_id, (
        "Combo must reference the existing exercise's id after replace too"
    )

    existing = page.evaluate(
        f"""async () => await window.__repos.exercises.getById('{existing_id}')"""
    )
    assert existing["default_reps"] == 99, "Existing exercise must be updated with imported data"

    assert _exercise_count_by_title(page, "Push-ups") == 1
    _clean_all(page)


# ============================================================
# Cross-user title conflicts (suffixed title collides with a previous import)
# ============================================================


def _register_suffix(page: Page, uuid: str, name: str, suffix: str):
    page.evaluate(
        """async ([uuid, name, suffix]) => {
            await window.__repos.suffixes.create({ user_uuid: uuid, user_name: name, suffix });
        }""",
        [uuid, name, suffix],
    )


def test_cross_user_title_conflict_skip(page: Page, app_url: str):
    """Re-importing from the same external user with a different id but the same pre-suffix
    title offers Replace/Skip (not a forced rename) against the previously-imported row;
    Skip leaves that row untouched."""
    _go_to_exercises(page, app_url)
    _clean_all(page)

    other_uuid = "other-user-uuid-conflict-1"
    previous_id = "ext-prev-id-1"
    _create_exercise_directly(page, "Squats [BS]", other_uuid, previous_id)
    _register_suffix(page, other_uuid, "Bob Smith", "BS")

    new_id = "ext-new-id-1"
    imported = {
        "id": new_id,
        "title": "Squats",
        "description": "Newer export",
        "asymmetric": False,
        "alternate": False,
        "image_urls": [],
        "video_urls": [],
        "default_reps": 50,
        "default_weight": 50,
        "created_by": other_uuid,
    }
    json_str = _build_export_json({"exercises": [imported]}, "Bob Smith", other_uuid)

    _trigger_import_with_json(page, json_str)

    # Suffix already registered, so it goes straight to preview
    expect(page.locator('[data-testid="import-preview-step"]')).to_be_visible(timeout=5000)
    page.click('[data-testid="import-apply"]')

    # The suffixed title ("Squats [BS]") collides with the previous import -> collisions screen
    expect(page.locator('[data-testid="import-collisions-step"]')).to_be_visible(timeout=5000)

    page.click('[data-testid="import-apply"]')
    expect(page.locator('[data-testid="import-report-step"]')).to_be_visible(timeout=5000)
    expect(page.locator('[data-testid="import-report-skipped"]')).to_be_visible()
    page.click('[data-testid="import-done"]')

    existing = page.evaluate(
        f"""async () => await window.__repos.exercises.getById('{previous_id}')"""
    )
    assert existing["default_reps"] == 10, "Previously-imported row must be unchanged"

    duplicate = page.evaluate(f"""async () => await window.__repos.exercises.getById('{new_id}')""")
    assert duplicate is None, "No new row should be created under the imported id"

    assert _exercise_count_by_title(page, "Squats [BS]") == 1
    _clean_all(page)


def test_cross_user_title_conflict_replace(page: Page, app_url: str):
    """Same scenario, resolved with Replace: the previously-imported row is updated in place."""
    _go_to_exercises(page, app_url)
    _clean_all(page)

    other_uuid = "other-user-uuid-conflict-2"
    previous_id = "ext-prev-id-2"
    _create_exercise_directly(page, "Lunges [BS]", other_uuid, previous_id)
    _register_suffix(page, other_uuid, "Bob Smith", "BS")

    new_id = "ext-new-id-2"
    imported = {
        "id": new_id,
        "title": "Lunges",
        "description": "Newer export",
        "asymmetric": False,
        "alternate": False,
        "image_urls": [],
        "video_urls": [],
        "default_reps": 50,
        "default_weight": 50,
        "created_by": other_uuid,
    }
    json_str = _build_export_json({"exercises": [imported]}, "Bob Smith", other_uuid)

    _trigger_import_with_json(page, json_str)
    expect(page.locator('[data-testid="import-preview-step"]')).to_be_visible(timeout=5000)
    page.click('[data-testid="import-apply"]')

    expect(page.locator('[data-testid="import-collisions-step"]')).to_be_visible(timeout=5000)
    page.click('[data-testid="collision-replace-exercise-0"]')
    page.click('[data-testid="import-apply"]')

    expect(page.locator('[data-testid="import-report-step"]')).to_be_visible(timeout=5000)
    expect(page.locator('[data-testid="import-report-exercises-replaced"]')).to_be_visible()
    page.click('[data-testid="import-done"]')

    existing = page.evaluate(
        f"""async () => await window.__repos.exercises.getById('{previous_id}')"""
    )
    assert existing is not None, "Previously-imported row must keep its id"
    assert existing["default_reps"] == 50, "Previously-imported row must be updated with new data"
    assert existing["title"] == "Lunges [BS]"

    duplicate = page.evaluate(f"""async () => await window.__repos.exercises.getById('{new_id}')""")
    assert duplicate is None, "No new row should be created under the imported id"

    assert _exercise_count_by_title(page, "Lunges [BS]") == 1
    _clean_all(page)
