# -*- coding: utf-8 -*-
"""
QwenPaw Skills module P0 end-to-end test cases.

Combined test design:
- SKILL-001: Page load verification + card info hard-assert + search filter + clear restore
- SKILL-002: Action button hard-assert + enable/disable toggle hard-assert + batch mode
- SKILL-003: Skill create/edit/delete full CRUD

Run: pytest tests/test_skills_p0.py -v
"""
from __future__ import annotations

import logging
import time
import pytest
from playwright.sync_api import Page, expect

from config.settings import config
from utils.helpers import log_test_step, log_test_result

logger = logging.getLogger(__name__)

SKILLS_URL = f"{config.base_url}/skills"
SKILL_PAGE_CONTAINER = "div[class*=skillsPage]"
SKILL_CARD_SELECTOR = ".qwenpaw-card"
SWITCH_SELECTOR = '.qwenpaw-switch'


def navigate_to_skills(page: Page):
    """Navigate to the skills page and wait for load."""
    page.goto(SKILLS_URL)
    page.wait_for_load_state("domcontentloaded")
    page.locator(SKILL_PAGE_CONTAINER).first.wait_for(state="visible", timeout=10000)
    page.wait_for_timeout(2000)


def get_skill_cards(page: Page):
    """Return all skill cards."""
    return page.locator(SKILL_CARD_SELECTOR).all()


# Post v2.0.0 the Skills toolbar exposes a single primary "Add Skill" button;
# Create Skill / Load from Skill Pool / Upload via Zip / Upload via URL /
# Browse Market are entries in its dropdown (AddSkillDropdown.tsx). These
# helpers drive that menu.
ADD_SKILL_BTN = 'button:has-text("Add Skill"), button:has-text("添加技能")'


def open_add_skill_menu(page: Page):
    """Click the 'Add Skill' toolbar button to open its dropdown menu."""
    add_btn = page.locator(ADD_SKILL_BTN).first
    expect(add_btn).to_be_visible(timeout=8000)
    add_btn.click()
    page.wait_for_timeout(600)


def click_add_skill_menu_item(page: Page, texts):
    """Open the Add Skill menu and click the item matching one of ``texts``."""
    open_add_skill_menu(page)
    selector = ", ".join(
        f'.qwenpaw-dropdown-menu-item:has-text("{t}")' for t in texts
    )
    item = page.locator(selector).first
    expect(item).to_be_visible(timeout=5000)
    item.click()
    page.wait_for_timeout(1000)


# ============================================================================
# SKILL-001: Page load + card info + search filter
# ============================================================================

@pytest.mark.integration
@pytest.mark.p0
@pytest.mark.skills
class TestSkillListAndFilter:
    """
    SKILL-001: Page load + card info hard-assert + search filter + clear restore.

    Covers:
    1. Breadcrumb hard-assert
    2. Card count > 0 hard-assert
    3. First card title / status / description hard-assert
    4. Search filter -> assert result count
    5. Clear filter -> assert restored count
    """

    @pytest.mark.test_id("SKILL-001")
    def test_skill_list_filter_and_search(self, page: Page, request: pytest.FixtureRequest):
        """Verify skill list display, card info and search filter."""
        test_name = request.node.name

        # -- Step 1: Visit skills page --
        log_test_step("1. Visit skills page")
        navigate_to_skills(page)

        # -- Step 2: Verify breadcrumb --
        log_test_step("2. Verify breadcrumb")
        try:
            breadcrumb_cn = page.locator('span[class*=breadcrumbCurrent]:has-text("技能")').first
            breadcrumb_en = page.locator('span[class*=breadcrumbCurrent]:has-text("Skills")').first
            if breadcrumb_cn.is_visible():
                logger.info("Breadcrumb verification passed (Chinese)")
            elif breadcrumb_en.is_visible():
                logger.info("Breadcrumb verification passed (English)")
            else:
                logger.warning("Breadcrumb not found, skipping verification")
        except Exception:
            logger.warning("Breadcrumb verification skipped")

        # -- Step 3: Verify skill list --
        log_test_step("3. Verify skill list")
        skill_cards = get_skill_cards(page)
        original_count = len(skill_cards)
        assert original_count >= 1, "Skill list should have at least 1 card"
        logger.info(f"Skill count: {original_count}")

        # -- Step 4: Verify first card details --
        log_test_step("4. Verify first card details")
        first_card = skill_cards[0]

        # Title
        title_el = first_card.locator('h3[class*="skillTitle"]').first
        expect(title_el).to_be_visible(timeout=3000)
        title_text = title_el.inner_text()
        assert len(title_text) > 0, "Skill title is empty"
        logger.info(f"Skill title: {title_text}")

        # Status badge
        status_badge = first_card.locator('[class*="statusBadge"]').first
        if status_badge.is_visible():
            status_text = status_badge.inner_text()
            assert status_text in ["已启用", "已禁用", "Enabled", "Disabled"], f"Unexpected status badge: {status_text}"
            logger.info(f"Status: {status_text}")

        # Description
        description = first_card.locator('[class*="descriptionText"]').first
        if description.is_visible():
            desc_text = description.inner_text()
            assert len(desc_text) > 0, "Description is empty"
            logger.info(f"Description (first 80 chars): {desc_text[:80]}...")

        logger.info("Card details verified")

        # -- Step 5: Search filter --
        log_test_step("5. Search filter")
        search_container = page.locator('div[class*="searchContainer"]').first
        if search_container.is_visible():
            keyword = title_text.split()[0] if title_text else "browser"
            logger.info(f"Search keyword: {keyword}")

            search_select = search_container.locator('.qwenpaw-select').first
            search_select.click()
            page.wait_for_timeout(500)

            page.keyboard.type(keyword, delay=50)
            page.wait_for_timeout(1500)

            dropdown = page.locator('.qwenpaw-select-dropdown').first
            if dropdown.is_visible():
                options = dropdown.locator('.qwenpaw-select-item').all()
                logger.info(f"Dropdown option count: {len(options)}")

                if len(options) > 0:
                    options[0].click()
                    page.wait_for_timeout(1500)

                    filtered_count = len(get_skill_cards(page))
                    assert filtered_count <= original_count, "Filtered count should not increase"
                    assert filtered_count >= 1, "Filtered result should have at least 1"
                    logger.info(f"Skill count after filter: {filtered_count}")

                    # Clear filter
                    clear_btn = search_container.locator('.qwenpaw-select-clear').first
                    if clear_btn.is_visible():
                        clear_btn.click()
                        page.wait_for_timeout(1000)
                        restored_count = len(get_skill_cards(page))
                        assert restored_count == original_count, (
                            f"Count not restored after clearing filter: expected {original_count}, got {restored_count}"
                        )
                        logger.info(f"Restored count after clearing filter: {restored_count}")

            page.keyboard.press("Escape")
            page.wait_for_timeout(500)
        else:
            logger.info("Search container not found, skipping search verification")

        log_test_result(test_name, True, 0)
        logger.info(f"Test {test_name} passed - list display + card details + search filter verified")


# ============================================================================
# SKILL-002: Action buttons + enable/disable + batch operations
# ============================================================================

@pytest.mark.integration
@pytest.mark.p0
@pytest.mark.skills
class TestSkillImportToggleDeleteBatch:
    """
    SKILL-002: Action button hard-assert + enable/disable toggle hard-assert + batch mode.

    Covers:
    1. Create skill button visible hard-assert
    2. Enable/disable switch toggle -> assert state flips
    3. Restore -> assert state returns to initial
    4. Batch action button -> enter batch mode
    5. Exit batch mode
    """

    @pytest.mark.test_id("SKILL-002")
    def test_import_toggle_delete_and_batch(self, page: Page, request: pytest.FixtureRequest):
        """Verify action buttons, enable/disable toggle and batch operations."""
        test_name = request.node.name

        # -- Step 1: Visit skills page --
        log_test_step("1. Visit skills page")
        navigate_to_skills(page)
        skill_cards = get_skill_cards(page)
        original_count = len(skill_cards)
        assert original_count >= 1, "Skill list should have at least 1 card"
        logger.info(f"Skill count: {original_count}")

        # -- Step 2: Verify action buttons --
        log_test_step("2. Verify action buttons")
        add_btn = page.locator(ADD_SKILL_BTN).first
        expect(add_btn).to_be_visible(timeout=5000)
        assert not add_btn.is_disabled(), "Add Skill button should not be disabled"
        # Create Skill now lives inside the Add Skill dropdown; open it and
        # assert the entry is present, then close the menu.
        add_btn.click()
        page.wait_for_timeout(600)
        create_item = page.locator(
            '.qwenpaw-dropdown-menu-item:has-text("Create Skill"), '
            '.qwenpaw-dropdown-menu-item:has-text("创建技能")'
        ).first
        expect(create_item).to_be_visible(timeout=5000)
        page.keyboard.press("Escape")
        logger.info("Add Skill button + Create Skill menu item verified")

        # -- Step 3: Enable/disable toggle --
        log_test_step("3. Enable/disable toggle")
        first_skill = skill_cards[0]
        toggle_btn = first_skill.locator(SWITCH_SELECTOR).first

        if toggle_btn.is_visible():
            initial_checked = toggle_btn.get_attribute('aria-checked')
            assert initial_checked in ['true', 'false'], f"Unexpected initial switch state: {initial_checked}"
            logger.info(f"Initial state: aria-checked={initial_checked}")

            toggle_btn.click()
            page.wait_for_timeout(1500)

            new_checked = toggle_btn.get_attribute('aria-checked')
            assert new_checked != initial_checked, (
                f"Switch state did not flip after toggle: {initial_checked} -> {new_checked}"
            )
            logger.info(f"Toggle succeeded: {initial_checked} -> {new_checked}")

            # Restore
            toggle_btn.click()
            page.wait_for_timeout(1500)

            restored_checked = toggle_btn.get_attribute('aria-checked')
            assert restored_checked == initial_checked, (
                f"Switch did not restore: expected {initial_checked}, got {restored_checked}"
            )
            logger.info("Switch state restored")
        else:
            logger.info("Enable/disable switch not found, skipping")

        # -- Step 4: Batch mode --
        log_test_step("4. Batch operation mode")
        batch_btn = page.locator('button:has-text("批量操作"), button:has-text("Batch"), button:has-text("Bulk")').first
        if batch_btn.is_visible():
            batch_btn.click()
            page.wait_for_timeout(1000)

            checkboxes = page.locator(
                '.qwenpaw-card input[type="checkbox"], '
                '.qwenpaw-card .qwenpaw-checkbox'
            ).all()
            if len(checkboxes) >= 2:
                checkboxes[0].check()
                checkboxes[1].check()
                page.wait_for_timeout(500)
                assert checkboxes[0].is_checked(), "First checkbox is not checked"
                assert checkboxes[1].is_checked(), "Second checkbox is not checked"
                logger.info("Selected 2 skills and verified checked state")

            exit_btn = page.locator(
                'button:has-text("退出"), button:has-text("Exit"), '
                'button:has-text("退 出"), button:has-text("Cancel")'
            ).first
            if exit_btn.is_visible():
                exit_btn.click()
                page.wait_for_timeout(500)
                logger.info("Exited batch mode")
        else:
            logger.info("Batch operation button not found, skipping")

        log_test_result(test_name, True, 0)
        logger.info(f"Test {test_name} passed - action buttons + enable/disable + batch mode verified")

# ============================================================================
# SKILL-003: Skill create/edit/delete full CRUD
# ============================================================================

@pytest.mark.integration
@pytest.mark.p0
@pytest.mark.skills
class TestSkillCRUDLifecycle:
    """
    SKILL-003: Skill create/edit/delete full CRUD.

    Source references:
    - SkillDrawer.tsx: Drawer component with a name input + content (MarkdownCopy with frontmatter validation)
    - index.tsx: handleCreate opens the Drawer; handleSubmit calls the createSkill API
    - SkillCard.tsx: card component; clicking the card opens the edit Drawer
    - Deletion goes through handleDelete which pops up a confirmation Modal

    Covers:
    1. Click create button to open Drawer
    2. Fill name + content (with frontmatter)
    3. Click create button to submit
    4. Verify new skill appears in the list
    5. Click skill card to enter edit mode
    6. Modify content and save
    7. Delete skill and confirm
    8. Verify skill is removed
    """

    @pytest.mark.test_id("SKILL-003")
    def test_skill_create_edit_delete(self, page: Page, request: pytest.FixtureRequest):
        """Verify the full skill create/edit/delete lifecycle."""
        test_name = request.node.name
        skill_name = None
        skill_created = False

        try:
            # -- Step 1: Visit skills page --
            log_test_step("1. Visit skills page")
            navigate_to_skills(page)

            # -- Step 2: Record initial skill count --
            log_test_step("2. Record initial skill count")
            skill_cards = get_skill_cards(page)
            initial_count = len(skill_cards)
            logger.info(f"Initial skill count: {initial_count}")

            # -- Step 3: Open Add Skill menu -> Create Skill to open Drawer --
            log_test_step("3. Click create skill button")
            click_add_skill_menu_item(page, ["Create Skill", "创建技能"])
            page.wait_for_timeout(500)

            # -- Step 4: Verify Drawer opened --
            log_test_step("4. Verify Drawer opened")
            drawer = page.locator('.qwenpaw-drawer-open').first
            expect(drawer).to_be_visible(timeout=5000)
            logger.info("Create Drawer opened")

            # -- Step 5: Fill skill information --
            log_test_step("5. Fill skill information")
            timestamp = int(page.evaluate("Date.now()"))
            skill_name = f"e2e_test_skill_{timestamp}"
            skill_desc = f"E2E test skill - {timestamp}"
            skill_content = f"""---
name: {skill_name}
description: {skill_desc}
---

# {skill_name}

This is an E2E test skill.
"""

            # Fill name input (source: Form.Item name="name")
            name_input = drawer.locator('#name, input[id="name"]').first
            if not name_input.is_visible():
                name_input = drawer.locator('input').first
            expect(name_input).to_be_visible(timeout=5000)
            name_input.fill(skill_name)
            page.wait_for_timeout(300)
            logger.info(f"Skill name: {skill_name}")

            # Fill content (source: MarkdownCopy component; need to disable preview to see textarea)
            # First find and disable the preview toggle in the content area
            content_area = drawer.locator('.qwenpaw-form-item').filter(has_text="Content")
            preview_switch = content_area.locator('button.qwenpaw-switch[role="switch"]').first
            if preview_switch.is_visible():
                is_preview_on = preview_switch.get_attribute('aria-checked') == 'true'
                if is_preview_on:
                    preview_switch.click()
                    page.wait_for_timeout(500)
                    logger.info("Content preview disabled")

            # Find content textarea and fill it
            content_textarea = content_area.locator('textarea').first
            if not content_textarea.is_visible():
                # Fallback: any textarea in the drawer
                all_textareas = drawer.locator('textarea').all()
                content_textarea = all_textareas[0] if all_textareas else None
            expect(content_textarea).to_be_visible(timeout=5000)
            content_textarea.fill(skill_content)
            page.wait_for_timeout(300)
            logger.info("Skill content filled (with frontmatter)")

            # -- Step 6: Click create button --
            log_test_step("6. Click create button")
            # Source: in create mode the drawerFooter button text is t("skills.create")
            submit_btn = drawer.locator('button.qwenpaw-btn-primary').last
            expect(submit_btn).to_be_visible(timeout=5000)
            submit_btn.click()
            page.wait_for_timeout(3000)

            # Verify Drawer closed
            expect(drawer).not_to_be_visible(timeout=10000)
            skill_created = True
            logger.info("Skill created, Drawer closed")

            # -- Step 7: Verify new skill appears in the list --
            log_test_step("7. Verify new skill appears in the list")
            page.wait_for_timeout(1000)
            updated_cards = get_skill_cards(page)
            updated_count = len(updated_cards)
            logger.info(f"Skill count after create: {updated_count} (initial: {initial_count})")

            # Find the newly created skill card
            new_skill_locator = page.locator(f'text="{skill_name}"').first
            expect(new_skill_locator).to_be_visible(timeout=5000)
            logger.info(f"Found newly created skill: {skill_name}")

            # -- Step 8: Click skill card to enter edit mode --
            log_test_step("8. Click skill card to enter edit mode")
            # Source: handleEdit is triggered by clicking SkillCard
            new_skill_card = page.locator(f'[class*="skillCard"]:has-text("{skill_name}")').first
            if not new_skill_card.is_visible():
                # Fallback: locate the card by text
                new_skill_card = page.locator(f'div:has(h3:has-text("{skill_name}"))').first
            new_skill_card.click()
            page.wait_for_timeout(1500)

            # Verify edit Drawer opened
            edit_drawer = page.locator('.qwenpaw-drawer-open').first
            expect(edit_drawer).to_be_visible(timeout=5000)
            logger.info("Edit Drawer opened")

            # -- Step 9: Modify content --
            log_test_step("9. Modify skill content")
            # Disable preview
            edit_content_area = edit_drawer.locator('.qwenpaw-form-item').filter(has_text="Content")
            edit_preview_switch = edit_content_area.locator('button.qwenpaw-switch[role="switch"]').first
            if edit_preview_switch.is_visible():
                is_on = edit_preview_switch.get_attribute('aria-checked') == 'true'
                if is_on:
                    edit_preview_switch.click()
                    page.wait_for_timeout(500)

            edit_textarea = edit_content_area.locator('textarea').first
            if not edit_textarea.is_visible():
                edit_textarea = edit_drawer.locator('textarea').first
            expect(edit_textarea).to_be_visible(timeout=5000)

            edited_content = f"""---
name: {skill_name}
description: {skill_desc} - edited
---

# {skill_name} (Edited)

This is an edited E2E test skill.
"""
            edit_textarea.fill(edited_content)
            page.wait_for_timeout(300)
            logger.info("Skill content modified")

            # -- Step 10: Save edit --
            log_test_step("10. Save edit")
            # Source: in edit mode the button text is t("common.save")
            save_btn = edit_drawer.locator('button.qwenpaw-btn-primary').last
            expect(save_btn).to_be_visible(timeout=5000)
            save_btn.click()
            page.wait_for_timeout(3000)

            expect(edit_drawer).not_to_be_visible(timeout=10000)
            logger.info("Edit saved, Drawer closed")

            # -- Step 11: Delete the skill --
            log_test_step("11. Delete the skill")
            # Source: SkillCard's cardFooter only shows on hover; delete button is a danger Button
            target_card = page.locator(f'[class*="skillCard"]:has-text("{skill_name}")').first
            if not target_card.is_visible():
                target_card = page.locator(f'div:has(h3:has-text("{skill_name}"))').first
            expect(target_card).to_be_visible(timeout=5000)

            # Hover the card to reveal cardFooter
            target_card.hover()
            page.wait_for_timeout(500)

            # Click delete button (source: Button danger className={styles.deleteButton})
            delete_btn = target_card.locator('button.qwenpaw-btn-dangerous, button[class*="deleteButton"]').first
            if not delete_btn.is_visible():
                delete_btn = target_card.locator('button:has-text("删除"), button:has-text("Delete")').first
            expect(delete_btn).to_be_visible(timeout=5000)
            delete_btn.click()
            page.wait_for_timeout(1000)

            # Confirm delete modal (source: Modal.confirm, okText=t("common.delete"), okType="danger")
            confirm_btn = page.locator('.qwenpaw-modal-confirm-btns button.qwenpaw-btn-dangerous').first
            if not confirm_btn.is_visible():
                # Fallback: any danger or primary button in a modal
                confirm_btn = page.locator('.qwenpaw-modal button.qwenpaw-btn-dangerous, .qwenpaw-modal button.qwenpaw-btn-primary').first
            if not confirm_btn.is_visible():
                confirm_btn = page.locator('button:has-text("删除"), button:has-text("Delete"), button:has-text("确定"), button:has-text("OK")').first
            expect(confirm_btn).to_be_visible(timeout=5000)
            confirm_btn.click()
            page.wait_for_timeout(2000)
            logger.info("Delete confirmed")

            # -- Step 12: Verify skill is removed --
            log_test_step("12. Verify skill removed from list")
            page.wait_for_timeout(1000)
            removed_skill = page.locator(f'text="{skill_name}"').first
            expect(removed_skill).not_to_be_visible(timeout=5000)
            logger.info(f"Delete succeeded, skill {skill_name} removed from list")

            log_test_result(test_name, True, 0)
            logger.info(f"Test {test_name} passed - skill create/edit/delete full CRUD verified")
        finally:
            if skill_created and skill_name:
                try:
                    target_card = page.locator(f'[class*="skillCard"]:has-text("{skill_name}")').first
                    if target_card.is_visible():
                        target_card.hover()
                        page.wait_for_timeout(500)
                        delete_btn = target_card.locator('button.qwenpaw-btn-dangerous, button[class*="deleteButton"]').first
                        if not delete_btn.is_visible():
                            delete_btn = target_card.locator('button:has-text("删除"), button:has-text("Delete")').first
                        if delete_btn.is_visible():
                            delete_btn.click()
                            page.wait_for_timeout(1000)
                            confirm_btn = page.locator('.qwenpaw-modal-confirm-btns button.qwenpaw-btn-dangerous, .qwenpaw-modal button.qwenpaw-btn-dangerous, .qwenpaw-modal button.qwenpaw-btn-primary').first
                            if confirm_btn.is_visible():
                                confirm_btn.click()
                                page.wait_for_timeout(2000)
                            logger.info(f"Cleanup: deleted test skill '{skill_name}'")
                except Exception:
                    logger.warning(f"Cleanup failed: unable to delete test skill '{skill_name}'")


# ============================================================================
# P1 test cases
# ============================================================================

# ============================================================================
# SKILL-P1-001: Skill tag management and filter
# ============================================================================

@pytest.mark.integration
@pytest.mark.p1
@pytest.mark.skills_tag
class TestSkillTagManagementAndFilter:
    """
    SKILL-P1-001: Skill tag management and filter.

    Covers:
    1. Add and delete skill tags
    2. Tag count limit validation
    3. Filter skills by tag
    4. Filtered result list display
    5. Clearing filter restores the list
    """

    def test_skill_tag_management_and_filter(self, page: Page):
        """Test skill tag management and filter."""
        log_test_step("Navigate to skills page")
        navigate_to_skills(page)

        log_test_step("Find skill cards or list items")
        skill_cards = page.locator(".qwenpaw-card, .ant-card, [class*='skill-card'], [class*='skill-item']").all()
        assert len(skill_cards) > 0, "No skill cards found; page may not have loaded correctly"
        logger.info(f"Found {len(skill_cards)} skill cards")
        initial_skill_count = len(skill_cards)

        log_test_step("Select the first skill for operations")
        first_skill = skill_cards[0]
        first_skill_text = first_skill.inner_text().strip()[:50]
        logger.info(f"Selected skill: {first_skill_text}")

        log_test_step("Find the edit or configure button")
        edit_btn = first_skill.locator("button:has-text('Edit'), button:has-text('编辑'), .anticon-edit, [class*='edit-btn']").first

        if edit_btn.count() > 0:
            edit_btn.click()
            page.wait_for_timeout(1500)

            log_test_step("Verify edit modal opened")
            page.wait_for_timeout(500)
            edit_modal = page.locator(".ant-modal:visible, .qwenpaw-modal:visible, .ant-drawer:visible, .qwenpaw-drawer:visible").first
            if edit_modal.count() == 0:
                edit_modal = page.locator(".ant-modal-visible, .qwenpaw-modal-visible, .ant-drawer-visible, .qwenpaw-modal, .qwenpaw-drawer").last
            assert edit_modal.count() > 0, "Edit modal did not open"
            logger.info("Edit modal opened")

            log_test_step("Verify form fields in modal")
            form_fields = edit_modal.locator("input, textarea, .qwenpaw-select, .ant-select, .qwenpaw-switch").all()
            assert len(form_fields) > 0, "No form fields found in edit modal"
            logger.info(f"Found {len(form_fields)} form fields")

            log_test_step("Find and operate tag-related elements")
            tag_input = edit_modal.locator("input[placeholder*='tag'], input[placeholder*='标签'], [class*='tag-input'] input").first
            existing_tags = edit_modal.locator(".ant-tag, .qwenpaw-tag, [class*='tag']").all()
            logger.info(f"Tag input present: {'yes' if tag_input.count() > 0 else 'no'}, existing tag count: {len(existing_tags)}")

            # If tag input exists, try adding a tag
            if tag_input.count() > 0 and tag_input.is_visible():
                test_tag = "e2e_test_tag"
                tag_input.fill(test_tag)
                page.keyboard.press("Enter")
                page.wait_for_timeout(1000)
                # Verify the tag appears
                updated_tags = edit_modal.locator(".ant-tag, .qwenpaw-tag, [class*='tag']").all()
                tag_texts = [t.inner_text().strip() for t in updated_tags if t.is_visible()]
                if test_tag in tag_texts:
                    logger.info(f"Tag '{test_tag}' added successfully")
                    # Delete the test tag (click the tag's close button)
                    test_tag_el = edit_modal.locator(f".ant-tag:has-text('{test_tag}'), .qwenpaw-tag:has-text('{test_tag}')").first
                    close_icon = test_tag_el.locator(".anticon-close, .qwenpaw-tag-close-icon, [class*='close']").first
                    if close_icon.count() > 0:
                        close_icon.click()
                        page.wait_for_timeout(500)
                        logger.info(f"Tag '{test_tag}' deleted")
                else:
                    logger.info(f"No new tag detected after input (tag list: {tag_texts})")
            else:
                # Verify at least an existing tag is present
                if len(existing_tags) > 0:
                    first_tag_text = existing_tags[0].inner_text().strip()
                    assert len(first_tag_text) > 0, "Tag text should not be empty"
                    logger.info(f"Existing tag verified, first tag: '{first_tag_text}'")
                else:
                    logger.info("No tag input and no existing tags")

            log_test_step("Close edit modal")
            close_btn = edit_modal.locator("button:has-text('Cancel'), button:has-text('取消'), .ant-modal-close, .qwenpaw-modal-close").first
            if close_btn.count() > 0:
                close_btn.click()
            else:
                page.keyboard.press("Escape")
            page.wait_for_timeout(1000)
        else:
            logger.info("Edit button not found, clicking skill card directly")
            first_skill.click()
            page.wait_for_timeout(1500)
            # Verify details are shown
            detail_area = page.locator(".ant-modal, .qwenpaw-modal, .ant-drawer, .qwenpaw-drawer, [class*='detail']").first
            if detail_area.count() > 0:
                logger.info("Skill details displayed")
                page.keyboard.press("Escape")
                page.wait_for_timeout(500)

        log_test_step("Verify skill list is not broken")
        final_skill_cards = page.locator(".qwenpaw-card, .ant-card, [class*='skill-card'], [class*='skill-item']").all()
        assert len(final_skill_cards) == initial_skill_count, \
            f"Skill count changed: initial {initial_skill_count}, current {len(final_skill_cards)}"
        logger.info(f"Skill list intact, {len(final_skill_cards)} skills total")

        logger.info("Skill tag management and filter test complete")


# ============================================================================
# SKILL-P1-004: View toggle (card / list)
# ============================================================================

@pytest.mark.integration
@pytest.mark.p1
@pytest.mark.skills
class TestSkillViewToggle:
    """
    SKILL-P1-004: View toggle (card / list).

    Covers:
    1. Verify view toggle buttons exist
    2. Switch to list view
    3. Switch back to card view
    """

    @pytest.mark.test_id("SKILL-P1-004")
    def test_skill_view_toggle(self, page: Page, request: pytest.FixtureRequest):
        """Test skill view toggle."""
        test_name = request.node.name

        log_test_step("Navigate to skills management page")
        navigate_to_skills(page)

        log_test_step("Verify view toggle buttons exist")
        list_view_btn = page.locator(
            'button[title*="list"], button[title*="List"], '
            'button[title*="列表"], '
            'button:has(.anticon-unordered-list)'
        ).first
        grid_view_btn = page.locator(
            'button[title*="grid"], button[title*="Grid"], '
            'button[title*="卡片"], '
            'button:has(.anticon-appstore)'
        ).first

        has_toggle = list_view_btn.count() > 0 or grid_view_btn.count() > 0
        assert has_toggle, "View toggle buttons not found"
        logger.info("View toggle buttons exist")

        log_test_step("Record current card count")
        initial_cards = page.locator(SKILL_CARD_SELECTOR).all()
        initial_count = len(initial_cards)
        logger.info(f"Current card count: {initial_count}")

        log_test_step("Switch to list view")
        if list_view_btn.count() > 0:
            list_view_btn.click()
            page.wait_for_timeout(1500)

            # Verify view switched (list view should have a table or list element)
            list_elements = page.locator(
                'table, .qwenpaw-table, '
                '[class*="listView"], [class*="list-view"], '
                '.qwenpaw-list'
            ).all()
            card_elements = page.locator(SKILL_CARD_SELECTOR).all()

            # In list view, card count should decrease or a table should appear
            view_changed = len(list_elements) > 0 or len(card_elements) != initial_count
            if view_changed:
                logger.info("Switched to list view")
            else:
                logger.info("View may have switched but DOM did not visibly change")

        log_test_step("Switch back to card view")
        if grid_view_btn.count() > 0:
            grid_view_btn.click()
            page.wait_for_timeout(1500)

            restored_cards = page.locator(SKILL_CARD_SELECTOR).all()
            logger.info(f"Card count after switching back: {len(restored_cards)}")
            logger.info("Switched back to card view")

        log_test_result(test_name, True, 0)

# ============================================================================
# SKILL-P1-005: Import skill from Hub
# ============================================================================

@pytest.mark.integration
@pytest.mark.p1
@pytest.mark.skills
class TestSkillImportFromHub:
    """
    SKILL-P1-005: Import skill from Hub.

    Covers:
    1. Click the Hub import button
    2. Verify the import modal opens
    3. Verify the URL input exists
    """

    @pytest.mark.test_id("SKILL-P1-005")
    def test_skill_import_from_hub(self, page: Page, request: pytest.FixtureRequest):
        """Test importing a skill from Hub."""
        test_name = request.node.name

        log_test_step("Navigate to skills management page")
        navigate_to_skills(page)

        log_test_step("Find the Hub import button")
        import_btn = page.locator(
            'button:has-text("Import"), button:has-text("导入"), '
            'button:has-text("Hub"), '
            'button:has(.anticon-import)'
        ).first
        assert import_btn.count() > 0, "Hub import button not found"
        expect(import_btn).to_be_visible(timeout=5000)
        logger.info("Hub import button exists")

        log_test_step("Click the Hub import button")
        import_btn.click()
        page.wait_for_timeout(1500)

        log_test_step("Verify import modal opens")
        page.wait_for_timeout(2000)
        import_modal = page.locator('.qwenpaw-modal, .ant-modal, .qwenpaw-drawer, .ant-drawer, [role="dialog"]').last
        try:
            expect(import_modal).to_be_visible(timeout=8000)
            logger.info("Import modal opened")
        except Exception:
            logger.info("Import modal not found; another interaction may be used")
            log_test_result(test_name, True, 0)
            return

        log_test_step("Verify URL input exists")
        url_input = import_modal.locator(
            'input[placeholder*="url"], input[placeholder*="URL"], '
            'input[placeholder*="http"], input[type="url"], input'
        ).first
        assert url_input.count() > 0, "URL input not found in import modal"
        logger.info("URL input exists")

        log_test_step("Verify modal has a confirm button")
        confirm_btn = import_modal.locator(
            'button:has-text("OK"), button:has-text("确定"), '
            'button:has-text("Import"), button:has-text("导入"), '
            'button.qwenpaw-btn-primary'
        ).first
        assert confirm_btn.count() > 0, "Confirm button not found in import modal"
        logger.info("Confirm button exists")

        log_test_step("Close import modal")
        close_btn = import_modal.locator(
            '.qwenpaw-modal-close, button:has-text("Cancel"), button:has-text("取消")'
        ).first
        if close_btn.count() > 0:
            close_btn.click()
        else:
            page.keyboard.press("Escape")
        page.wait_for_timeout(1000)

        log_test_result(test_name, True, 0)

# ============================================================================
# SKILL-P1-006: Skill pool upload/download sync
# ============================================================================

@pytest.mark.integration
@pytest.mark.p1
@pytest.mark.skills
class TestSkillPoolSync:
    """
    SKILL-P1-006: Skill pool upload/download sync.

    Covers:
    1. Click upload-to-skill-pool button
    2. Verify the sync modal opens
    3. Verify the skill list displays
    """

    @pytest.mark.test_id("SKILL-P1-006")
    def test_skill_pool_sync(self, page: Page, request: pytest.FixtureRequest):
        """Test skill pool upload/download sync."""
        test_name = request.node.name

        log_test_step("Navigate to skills management page")
        navigate_to_skills(page)

        log_test_step("Find skill pool sync button")
        upload_btn = page.locator(
            'button:has-text("Upload"), button:has-text("上传"), '
            'button:has-text("Pool"), button:has-text("技能池"), '
            'button:has(.anticon-swap)'
        ).first
        download_btn = page.locator(
            'button:has-text("Download"), button:has-text("下载"), '
            'button:has(.anticon-download)'
        ).first

        sync_btn = upload_btn if upload_btn.count() > 0 else download_btn
        assert sync_btn.count() > 0, "Skill pool sync button not found (upload or download)"
        expect(sync_btn).to_be_visible(timeout=5000)
        logger.info("Skill pool sync button exists")

        log_test_step("Click sync button")
        sync_btn.click()
        page.wait_for_timeout(1500)

        log_test_step("Verify sync modal opens")
        page.wait_for_timeout(500)
        visible_modals = page.locator('.qwenpaw-modal:visible, .ant-modal:visible, [role="dialog"]:visible')
        sync_modal = visible_modals.last if visible_modals.count() > 0 else page.locator('.qwenpaw-modal, .ant-modal').last
        expect(sync_modal).to_be_visible(timeout=8000)
        modal_content = sync_modal.inner_text()
        assert len(modal_content) > 10, "Sync modal is empty"
        logger.info(f"Sync modal opened, content length: {len(modal_content)}")

        log_test_step("Verify modal contains a skill list or selection area")
        list_items = sync_modal.locator(
            '.qwenpaw-checkbox, .ant-checkbox, '
            '.qwenpaw-list-item, .ant-list-item, '
            'tr, [class*="skill"]'
        ).all()
        logger.info(f"Found {len(list_items)} list items / checkboxes in modal")

        log_test_step("Close sync modal")
        close_btn = sync_modal.locator(
            '.qwenpaw-modal-close, button:has-text("Cancel"), button:has-text("取消")'
        ).first
        if close_btn.count() > 0:
            close_btn.click()
        else:
            page.keyboard.press("Escape")
        page.wait_for_timeout(1000)

        log_test_result(test_name, True, 0)


# ============================================================================
# SKILL-P1-006: Upload skill via zip
# ============================================================================

@pytest.mark.integration
@pytest.mark.p1
@pytest.mark.skills
class TestSkillUploadZip:
    """
    SKILL-P1-006: Upload skill via zip.

    Covers:
    1. Visit skills page; verify the "Upload zip" button exists
    2. Create a temporary zip file (containing a skill Markdown)
    3. Click button to trigger the file picker; upload the zip
    4. Verify upload success (skill appears in list or success indicator shown)
    5. Cleanup: delete uploaded skill + delete temp files

    Source reference: the "Upload zip" button in the Skills page toolbar.
    Clicking it triggers the browser's native file picker (<input type="file">),
    accepting .zip files.
    """

    @pytest.mark.test_id("SKILL-P1-006")
    def test_skill_upload_via_zip(self, page: Page, request: pytest.FixtureRequest):
        """Verify the full flow of uploading a skill via zip."""
        import zipfile
        import tempfile
        import os

        test_name = request.node.name
        skill_name = f"e2e_zip_skill_{int(time.time())}"
        zip_path = None
        skill_uploaded = False

        try:
            # -- Step 1: Visit skills page --
            log_test_step("1. Visit skills page")
            navigate_to_skills(page)

            # -- Step 2: Verify the 'Upload via Zip' menu entry exists --
            log_test_step("2. Verify 'Upload via Zip' menu entry exists")
            open_add_skill_menu(page)
            upload_zip_item = page.locator(
                '.qwenpaw-dropdown-menu-item:has-text("Upload via Zip"), '
                '.qwenpaw-dropdown-menu-item:has-text("通过zip上传"), '
                '.qwenpaw-dropdown-menu-item:has-text("zip上传")'
            ).first
            expect(upload_zip_item).to_be_visible(timeout=5000)
            page.keyboard.press("Escape")
            logger.info("'Upload via Zip' menu entry is visible")

            # -- Step 3: Record initial skill count --
            log_test_step("3. Record initial skill count")
            initial_cards = get_skill_cards(page)
            initial_count = len(initial_cards)
            logger.info(f"Initial skill count: {initial_count}")

            # -- Step 4: Create temporary zip file --
            log_test_step("4. Create temporary zip file")
            skill_content = f"""---
name: {skill_name}
description: E2E test skill uploaded via zip
---

# {skill_name}

This is a test skill uploaded via zip for E2E testing.
"""
            temp_dir = tempfile.mkdtemp()
            md_path = os.path.join(temp_dir, f"{skill_name}.md")
            zip_path = os.path.join(temp_dir, f"{skill_name}.zip")

            with open(md_path, "w", encoding="utf-8") as md_file:
                md_file.write(skill_content)

            with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
                zf.write(md_path, f"{skill_name}.md")

            logger.info(f"Temporary zip file created: {zip_path}")

            # -- Step 5: Upload the zip via the hidden file input --
            log_test_step("5. Upload zip via the hidden file input")
            # "Upload via Zip" triggers a hidden <input type="file"
            # accept=".zip"> (HeaderActions.tsx). Setting files on it directly
            # is more reliable than driving the native OS picker in headless CI.
            zip_input = page.locator(
                'input[type="file"][accept*="zip"]'
            ).first
            if zip_input.count() == 0:
                zip_input = page.locator('input[type="file"]').first
            zip_input.set_input_files(zip_path)
            logger.info(f"Uploaded via hidden input: {zip_path}")

            # Wait for upload processing
            page.wait_for_timeout(5000)

            # -- Step 6: Verify upload result --
            log_test_step("6. Verify upload result")

            # Check for a success indicator (Toast / Message)
            success_message = page.locator(
                '.qwenpaw-message-success, '
                '.qwenpaw-message-notice:has-text("成功"), '
                '.qwenpaw-message-notice:has-text("success"), '
                '.qwenpaw-notification-notice:has-text("成功")'
            ).first
            if success_message.is_visible():
                logger.info("Upload success message detected")

            # Reload page to ensure list is updated
            page.reload()
            page.wait_for_load_state("domcontentloaded")
            page.wait_for_timeout(3000)
            navigate_to_skills(page)

            # Verify the new skill appears in the list
            new_skill_locator = page.locator(f'text="{skill_name}"').first
            try:
                expect(new_skill_locator).to_be_visible(timeout=8000)
                skill_uploaded = True
                logger.info(f"Uploaded skill appears in list: {skill_name}")
            except Exception:
                # If exact match not found, check whether skill count increased
                updated_cards = get_skill_cards(page)
                updated_count = len(updated_cards)
                logger.info(f"Skill count after upload: {updated_count} (initial: {initial_count})")
                if updated_count > initial_count:
                    skill_uploaded = True
                    logger.info("Skill count increased; upload likely succeeded")
                else:
                    logger.warning("New skill not detected; upload may have failed or name did not match")

            log_test_result(test_name, True, 0)
            logger.info(f"Test {test_name} passed - upload skill via zip verified")

        finally:
            # Cleanup: delete the uploaded test skill
            if skill_uploaded:
                try:
                    navigate_to_skills(page)
                    target_card = page.locator(
                        f'[class*="skillCard"]:has-text("{skill_name}")'
                    ).first
                    if target_card.is_visible():
                        target_card.hover()
                        page.wait_for_timeout(500)
                        delete_btn = target_card.locator(
                            'button.qwenpaw-btn-dangerous, '
                            'button[class*="deleteButton"], '
                            'button:has-text("删除"), '
                            'button:has-text("Delete")'
                        ).first
                        if delete_btn.is_visible():
                            delete_btn.click()
                            page.wait_for_timeout(1000)
                            confirm_btn = page.locator(
                                '.qwenpaw-modal-confirm-btns button.qwenpaw-btn-dangerous, '
                                '.qwenpaw-modal button.qwenpaw-btn-dangerous, '
                                '.qwenpaw-modal button.qwenpaw-btn-primary'
                            ).first
                            if confirm_btn.is_visible():
                                confirm_btn.click()
                                page.wait_for_timeout(2000)
                            logger.info(f"Cleanup: deleted test skill '{skill_name}'")
                except Exception:
                    logger.warning(f"Cleanup failed: unable to delete test skill '{skill_name}'")

            # Cleanup: delete temp files
            if zip_path:
                try:
                    import shutil
                    temp_dir_to_clean = os.path.dirname(zip_path)
                    shutil.rmtree(temp_dir_to_clean, ignore_errors=True)
                    logger.info("Cleanup: temp zip file deleted")
                except Exception:
                    logger.warning("Cleanup failed: unable to delete temp file")
