from datetime import datetime, timedelta, timezone

import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

import app.models  # noqa: F401
from app.database import Base, get_db
from app.models.conversation import Conversation
from app.models.user import User
from app.routers.agent import router as agent_router
from app.routers.auth import get_current_user
from app.services.auth_service import get_password_hash


@pytest.fixture
async def visibility_env(tmp_path):
    db_path = tmp_path / 'test_visibility_api.db'
    engine = create_async_engine(f'sqlite+aiosqlite:///{db_path}', future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    now = datetime.now(timezone.utc)
    snapshot = {
        'kind': 'agent_continuity_state',
        'version': 1,
        'state': {
            'agent_id': 'master',
            'root_agent_id': 'master',
            'current_agent': 'analyst-1234abcd',
            'thread_id': 'thread-1',
            'spawned_agent_ids': ['analyst-1234abcd'],
            'event_trace': [
                {
                    'event_id': 'evt-1',
                    'event_type': 'agent.created',
                    'timestamp': (now - timedelta(minutes=10)).isoformat(),
                    'conversation_id': 'placeholder',
                    'agent_id': 'master',
                    'thread_id': 'thread-1',
                    'task_id': 'task-1',
                    'payload': {'child_agent_id': 'analyst-1234abcd'},
                    'severity': 'info',
                },
                {
                    'event_id': 'evt-2',
                    'event_type': 'agent.tool.result',
                    'timestamp': (now - timedelta(minutes=5)).isoformat(),
                    'conversation_id': 'placeholder',
                    'agent_id': 'analyst-1234abcd',
                    'thread_id': 'thread-1',
                    'task_id': 'task-1',
                    'payload': {'tool_name': 'search_web', 'result_preview': 'ok'},
                    'severity': 'info',
                },
            ],
            'message_trace': [
                {
                    'message_id': 'msg-1',
                    'thread_id': 'thread-1',
                    'from_agent_id': 'master',
                    'to_agent_id': 'analyst-1234abcd',
                    'task_id': 'task-1',
                    'message_type': 'task_request',
                    'content_summary': 'Analyze the issue',
                    'created_at': (now - timedelta(minutes=9)).isoformat(),
                    'payload': {},
                },
                {
                    'message_id': 'msg-2',
                    'thread_id': 'thread-1',
                    'from_agent_id': 'analyst-1234abcd',
                    'to_agent_id': 'master',
                    'task_id': 'task-1',
                    'reply_to_message_id': 'msg-1',
                    'message_type': 'task_update',
                    'content_summary': 'Done',
                    'created_at': (now - timedelta(minutes=4)).isoformat(),
                    'payload': {'status': 'completed'},
                },
            ],
            'active_tasks': [
                {
                    'task_id': 'task-1',
                    'title': 'Analyze issue',
                    'role': 'analyst',
                    'owner_agent_id': 'analyst-1234abcd',
                    'status': 'completed',
                    'thread_id': 'thread-1',
                    'result_summary': 'Analysis complete',
                    'evidence': [
                        {
                            'tool_name': 'search_web',
                            'args': {'query': 'jarvis visibility'},
                            'result_preview': 'ok',
                        }
                    ],
                }
            ],
            'task_results': [
                {
                    'task_id': 'task-1',
                    'status': 'completed',
                    'summary': 'Analysis complete',
                    'owner_agent_id': 'analyst-1234abcd',
                    'thread_id': 'thread-1',
                    'evidence': [
                        {
                            'tool_name': 'search_web',
                            'args': {'query': 'jarvis visibility'},
                            'result_preview': 'ok',
                        },
                        {
                            'type': 'verification',
                            'status': 'passed',
                            'summary': 'Verified',
                        },
                    ],
                }
            ],
            'task_hierarchy': {'root-task': ['task-1']},
            'tool_outcomes': [
                {
                    'tool_name': 'search_web',
                    'args': {'query': 'jarvis visibility'},
                    'result_preview': 'ok',
                    'verifier_hints': {'tool_name': 'search_web'},
                }
            ],
            'verification_status': 'passed',
            'verification_summary': 'All task evidence verified.',
            'verification_evidence': [
                {'task_id': 'task-1', 'status': 'passed', 'summary': 'Verified'}
            ],
            'execution_mode': 'collaboration',
            'current_phase': 'phase_4_visibility_and_verification',
            'current_checkpoint': 'visibility.runtime_summary_ready',
            'phase_history': [
                {'phase': 'phase_0_bootstrap'},
                {'phase': 'phase_4_visibility_and_verification'},
            ],
            'checkpoint_history': [
                {'checkpoint': 'bootstrap.initialized'},
                {'checkpoint': 'visibility.runtime_summary_ready'},
            ],
            'input_tokens': 120,
            'output_tokens': 80,
            'budget_warning': True,
            'estimated_cost': 0.00156,
            'cost_thresholds': {'total_tokens': 150, 'estimated_cost': 0.001},
            'cost_by_agent': {
                'master': {
                    'agent_id': 'master',
                    'input_tokens': 60,
                    'output_tokens': 20,
                    'total_tokens': 80,
                    'estimated_cost': 0.00048,
                    'budget_warning': False,
                },
                'analyst-1234abcd': {
                    'agent_id': 'analyst-1234abcd',
                    'input_tokens': 60,
                    'output_tokens': 60,
                    'total_tokens': 120,
                    'estimated_cost': 0.00108,
                    'budget_warning': True,
                },
            },
            'isolation_mode': 'worktree',
            'isolation_id': 'iso-1',
            'isolation_workspace_path': '/tmp/jarvis/worktree-1',
            'isolation_parent_conversation_id': 'parent-conv-1',
            'isolation_metadata': {'branch': 'jarvis/test-worker'},
        },
    }

    async with session_factory() as session:
        user = User(
            username='visibility_user',
            email='visibility@example.com',
            hashed_password=get_password_hash('secret123'),
            full_name='Visibility Tester',
        )
        session.add(user)
        await session.flush()
        conversation = Conversation(user_id=user.id, title='Visibility test', agent_state=snapshot)
        session.add(conversation)
        await session.commit()
        await session.refresh(user)
        await session.refresh(conversation)

        snapshot['state']['event_trace'][0]['conversation_id'] = conversation.id
        snapshot['state']['event_trace'][1]['conversation_id'] = conversation.id
        conversation.agent_state = snapshot
        await session.commit()
        await session.refresh(conversation)

    async def override_get_db():
        async with session_factory() as session:
            yield session

    async def override_get_current_user():
        return user

    test_app = FastAPI()
    test_app.include_router(agent_router)
    test_app.dependency_overrides[get_db] = override_get_db
    test_app.dependency_overrides[get_current_user] = override_get_current_user

    try:
        yield test_app, {
            'conversation_id': conversation.id,
            'thread_id': 'thread-1',
            'task_id': 'task-1',
            'started_after': (now - timedelta(minutes=11)).isoformat(),
            'ended_before': (now - timedelta(minutes=1)).isoformat(),
        }
    finally:
        await engine.dispose()


@pytest.mark.asyncio
async def test_visibility_events_support_filters_and_pagination(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/events',
            params={
                'conversation_id': ids['conversation_id'],
                'agent_id': 'analyst-1234abcd',
                'thread_id': ids['thread_id'],
                'event_type': 'agent.tool.result',
                'limit': 1,
                'offset': 0,
            },
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['total'] == 1
    assert payload['limit'] == 1
    assert payload['items'][0]['event_id'] == 'evt-2'


@pytest.mark.asyncio
async def test_visibility_topology_returns_nodes_edges_and_task_summary(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/topology',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['root_agent_id'] == 'master'
    assert payload['current_agent'] == 'analyst-1234abcd'
    assert any(node['agent_id'] == 'analyst-1234abcd' for node in payload['nodes'])
    assert any(edge['child_agent_id'] == 'analyst-1234abcd' for edge in payload['edges'])
    assert payload['tasks'][0]['task_id'] == ids['task_id']
    assert payload['task_hierarchy'] == {'root-task': ['task-1']}


@pytest.mark.asyncio
async def test_visibility_task_evidence_returns_tool_and_verifier_evidence(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            f'/api/agents/visibility/tasks/{ids["task_id"]}/evidence',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['task']['task_id'] == ids['task_id']
    assert payload['result']['status'] == 'completed'
    assert payload['tool_outcomes'][0]['tool_name'] == 'search_web'
    assert payload['verifier']['status'] == 'passed'


@pytest.mark.asyncio
async def test_visibility_task_evidence_uses_task_evidence_instead_of_global_tool_outcomes(tmp_path):
    db_path = tmp_path / 'test_visibility_api_task_evidence_filter.db'
    engine = create_async_engine(f'sqlite+aiosqlite:///{db_path}', future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    snapshot = {
        'kind': 'agent_continuity_state',
        'version': 1,
        'state': {
            'agent_id': 'master',
            'root_agent_id': 'master',
            'current_agent': 'analyst-1234abcd',
            'thread_id': 'thread-1',
            'spawned_agent_ids': ['analyst-1234abcd'],
            'event_trace': [],
            'message_trace': [],
            'active_tasks': [
                {
                    'task_id': 'task-1',
                    'title': 'Analyze issue',
                    'role': 'analyst',
                    'owner_agent_id': 'analyst-1234abcd',
                    'status': 'completed',
                    'thread_id': 'thread-1',
                    'result_summary': 'Analysis complete',
                    'evidence': [],
                }
            ],
            'task_results': [
                {
                    'task_id': 'task-1',
                    'status': 'completed',
                    'summary': 'Analysis complete',
                    'owner_agent_id': 'analyst-1234abcd',
                    'thread_id': 'thread-1',
                    'evidence': [
                        {
                            'tool_name': 'search_web',
                            'args': {'query': 'jarvis visibility'},
                            'result_preview': 'task-specific',
                        },
                        {
                            'type': 'verification',
                            'status': 'passed',
                            'summary': 'Verified',
                        },
                    ],
                }
            ],
            'task_hierarchy': {'root-task': ['task-1']},
            'tool_outcomes': [
                {
                    'tool_name': 'search_web',
                    'args': {'query': 'jarvis visibility'},
                    'result_preview': 'global-duplicate',
                    'verifier_hints': {'tool_name': 'search_web'},
                }
            ],
            'verification_status': 'passed',
            'verification_summary': 'All task evidence verified.',
            'verification_evidence': [
                {'task_id': 'task-1', 'status': 'passed', 'summary': 'Verified'}
            ],
        },
    }

    async with session_factory() as session:
        user = User(
            username='task_evidence_user',
            email='task-evidence@example.com',
            hashed_password=get_password_hash('secret123'),
            full_name='Task Evidence Tester',
        )
        session.add(user)
        await session.flush()
        conversation = Conversation(user_id=user.id, title='Task evidence test', agent_state=snapshot)
        session.add(conversation)
        await session.commit()
        await session.refresh(user)
        await session.refresh(conversation)

    async def override_get_db():
        async with session_factory() as session:
            yield session

    async def override_get_current_user():
        return user

    test_app = FastAPI()
    test_app.include_router(agent_router)
    test_app.dependency_overrides[get_db] = override_get_db
    test_app.dependency_overrides[get_current_user] = override_get_current_user

    transport = ASGITransport(app=test_app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/tasks/task-1/evidence',
            params={'conversation_id': conversation.id},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['tool_outcomes'] == [
        {
            'tool_name': 'search_web',
            'args': {'query': 'jarvis visibility'},
            'result_preview': 'task-specific',
        }
    ]

    await engine.dispose()


@pytest.mark.asyncio
async def test_visibility_thread_messages_returns_thread_history(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            f'/api/agents/visibility/threads/{ids["thread_id"]}/messages',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['thread_id'] == ids['thread_id']
    assert payload['total'] == 2
    assert payload['items'][1]['reply_to_message_id'] == 'msg-1'


@pytest.mark.asyncio
async def test_visibility_verifier_returns_verdict(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/verifier',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['status'] == 'passed'
    assert payload['summary'] == 'All task evidence verified.'
    assert payload['evidence'][0]['task_id'] == ids['task_id']


@pytest.mark.asyncio
async def test_visibility_runtime_summary_returns_phase_cost_and_isolation_metadata(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/runtime-summary',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['conversation_id'] == ids['conversation_id']
    assert payload['execution_mode'] == 'collaboration'
    assert payload['current_phase'] == 'phase_4_visibility_and_verification'
    assert payload['current_checkpoint'] == 'visibility.runtime_summary_ready'
    assert payload['verifier']['status'] == 'passed'
    assert payload['isolation']['mode'] == 'worktree'
    assert payload['isolation']['workspace_path'] == '/tmp/jarvis/worktree-1'
    assert payload['isolation']['metadata']['branch'] == 'jarvis/test-worker'
    assert payload['cost']['input_tokens'] == 120
    assert payload['cost']['output_tokens'] == 80
    assert payload['cost']['total_tokens'] == 200
    assert payload['cost']['estimated_cost'] == 0.00156
    assert payload['cost']['budget_warning'] is True
    assert payload['topology_node_count'] == 2
    assert payload['active_task_count'] == 1
    assert payload['completed_task_count'] == 1
    assert payload['recent_events'][0]['event_id'] == 'evt-1'


@pytest.mark.asyncio
async def test_visibility_cost_returns_totals_thresholds_and_agent_breakdown(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/cost',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['total']['input_tokens'] == 120
    assert payload['total']['output_tokens'] == 80
    assert payload['total']['total_tokens'] == 200
    assert payload['total']['budget_warning'] is True
    assert payload['thresholds']['total_tokens'] == 150
    assert payload['thresholds']['estimated_cost'] == 0.001
    assert payload['by_agent'][0]['agent_id'] == 'analyst-1234abcd'
    assert payload['by_agent'][0]['budget_warning'] is True
    assert payload['by_agent'][1]['agent_id'] == 'master'


@pytest.mark.asyncio
async def test_visibility_tools_returns_governance_metadata_and_usage_counts(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/tools',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['total_tools'] >= 1
    assert payload['used_tools'] >= 1
    search_tool = next(item for item in payload['items'] if item['tool_name'] == 'search_web')
    assert search_tool['permission_class'] == 'external'
    assert search_tool['side_effect_scope'] == 'network'
    assert search_tool['usage_count'] == 1
    assert search_tool['last_result_preview'] == 'ok'
    assert payload['upgrade_candidates'] == [
        'worktree_manager',
        'cost_inspector',
        'runtime_event_drilldown',
        'tool_policy_explorer',
    ]


@pytest.mark.asyncio
async def test_visibility_events_reject_invalid_datetime(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/events',
            params={
                'conversation_id': ids['conversation_id'],
                'started_after': 'not-a-date',
            },
        )

    assert response.status_code == 400
    assert response.json()['detail'] == '时间参数必须是 ISO 8601 格式'


@pytest.mark.asyncio
async def test_visibility_events_support_time_window_and_offset_pagination(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/events',
            params={
                'conversation_id': ids['conversation_id'],
                'started_after': ids['started_after'],
                'ended_before': ids['ended_before'],
                'limit': 1,
                'offset': 1,
            },
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['total'] == 2
    assert payload['limit'] == 1
    assert payload['offset'] == 1
    assert len(payload['items']) == 1
    assert payload['items'][0]['event_id'] == 'evt-2'


@pytest.mark.asyncio
async def test_visibility_topology_includes_task_counts_for_root_and_child(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/topology',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 200
    payload = response.json()
    nodes = {node['agent_id']: node for node in payload['nodes']}
    assert nodes['master']['task_count'] == 0
    assert nodes['master']['completed_task_count'] == 0
    assert nodes['analyst-1234abcd']['task_count'] == 1
    assert nodes['analyst-1234abcd']['completed_task_count'] == 1


@pytest.mark.asyncio
async def test_visibility_task_evidence_returns_404_for_unknown_task(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/tasks/missing-task/evidence',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 404
    assert response.json()['detail'] == '任务不存在'


@pytest.mark.asyncio
async def test_visibility_thread_messages_returns_404_for_unknown_thread(visibility_env):
    app, ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/threads/missing-thread/messages',
            params={'conversation_id': ids['conversation_id']},
        )

    assert response.status_code == 404
    assert response.json()['detail'] == '线程不存在'


@pytest.mark.asyncio
async def test_visibility_returns_404_when_conversation_is_missing(visibility_env):
    app, _ids = visibility_env
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/events',
            params={'conversation_id': 'missing-conversation'},
        )

    assert response.status_code == 404
    assert response.json()['detail'] == '对话不存在'


@pytest.mark.asyncio
async def test_visibility_returns_404_when_snapshot_is_missing(tmp_path):
    db_path = tmp_path / 'test_visibility_api_missing_snapshot.db'
    engine = create_async_engine(f'sqlite+aiosqlite:///{db_path}', future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with session_factory() as session:
        user = User(
            username='missing_snapshot_user',
            email='missing-snapshot@example.com',
            hashed_password=get_password_hash('secret123'),
            full_name='Missing Snapshot Tester',
        )
        session.add(user)
        await session.flush()
        conversation = Conversation(user_id=user.id, title='Missing snapshot test', agent_state=None)
        session.add(conversation)
        await session.commit()
        await session.refresh(user)
        await session.refresh(conversation)

    async def override_get_db():
        async with session_factory() as session:
            yield session

    async def override_get_current_user():
        return user

    test_app = FastAPI()
    test_app.include_router(agent_router)
    test_app.dependency_overrides[get_db] = override_get_db
    test_app.dependency_overrides[get_current_user] = override_get_current_user

    transport = ASGITransport(app=test_app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/verifier',
            params={'conversation_id': conversation.id},
        )

    assert response.status_code == 404
    assert response.json()['detail'] == '当前会话暂无可视化运行时数据'

    await engine.dispose()


@pytest.mark.asyncio
async def test_visibility_verifier_returns_empty_verdict_when_state_is_unverified(tmp_path):
    db_path = tmp_path / 'test_visibility_api_empty_verifier.db'
    engine = create_async_engine(f'sqlite+aiosqlite:///{db_path}', future=True)
    session_factory = async_sessionmaker(engine, expire_on_commit=False)

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    snapshot = {
        'kind': 'agent_continuity_state',
        'version': 1,
        'state': {
            'agent_id': 'master',
            'root_agent_id': 'master',
            'current_agent': 'master',
            'event_trace': [],
            'message_trace': [],
            'active_tasks': [],
            'task_results': [],
            'task_hierarchy': {},
            'tool_outcomes': [],
            'verification_status': None,
            'verification_summary': None,
            'verification_evidence': [],
        },
    }

    async with session_factory() as session:
        user = User(
            username='empty_verifier_user',
            email='empty-verifier@example.com',
            hashed_password=get_password_hash('secret123'),
            full_name='Empty Verifier Tester',
        )
        session.add(user)
        await session.flush()
        conversation = Conversation(user_id=user.id, title='Empty verifier test', agent_state=snapshot)
        session.add(conversation)
        await session.commit()
        await session.refresh(user)
        await session.refresh(conversation)

    async def override_get_db():
        async with session_factory() as session:
            yield session

    async def override_get_current_user():
        return user

    test_app = FastAPI()
    test_app.include_router(agent_router)
    test_app.dependency_overrides[get_db] = override_get_db
    test_app.dependency_overrides[get_current_user] = override_get_current_user

    transport = ASGITransport(app=test_app)
    async with AsyncClient(transport=transport, base_url='http://testserver') as client:
        response = await client.get(
            '/api/agents/visibility/verifier',
            params={'conversation_id': conversation.id},
        )

    assert response.status_code == 200
    payload = response.json()
    assert payload['status'] is None
    assert payload['summary'] is None
    assert payload['evidence'] == []

    await engine.dispose()
