import json
import tempfile

import pytest
from aiohttp import web
from aiohttp.test_utils import make_mocked_request
from mjr_am_backend.features.workflows import service as workflows_service
from mjr_am_backend.routes.handlers import workflows as workflows_routes
from mjr_am_backend.shared import Result


def _build_app() -> web.Application:
    app = web.Application()
    routes = web.RouteTableDef()
    workflows_routes.register_workflow_routes(routes)
    app.add_routes(routes)
    return app


@pytest.mark.asyncio
async def test_workflow_content_rejects_invalid_path(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    workflow_dir.mkdir()
    outside = tmp_path / "outside.json"
    outside.write_text(json.dumps({"nodes": []}), encoding="utf-8")
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))

    app = _build_app()
    req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/content?filepath={outside.as_posix()}",
        app=app,
    )
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "FORBIDDEN"


@pytest.mark.asyncio
async def test_workflow_validate_returns_diagnostics(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    workflow_dir.mkdir()
    workflow_path = workflow_dir / "validate.json"
    workflow_path.write_text(
        json.dumps(
            {
                "name": "Validate",
                "nodes": [
                    {"id": 1, "type": "CheckpointLoaderSimple", "widgets_values": ["model.safetensors"]},
                    {"id": 2, "type": "RemoteRunApi"},
                    {"id": 3, "type": "GroupNode", "subgraph": {"nodes": [{"id": 1, "type": "KSampler"}]}},
                ],
                "links": [],
            }
        ),
        encoding="utf-8",
    )
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))

    app = _build_app()
    req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/validate?filepath={workflow_path.as_posix()}",
        app=app,
    )
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is True
    data = body.get("data", {})
    assert data["valid"] is True
    assert data["node_count"] == 4
    assert data["subgraph_count"] == 1
    assert "RemoteRunApi" in data["api_nodes"]
    assert "model.safetensors" in data["required_models"]
    assert any(item.endswith("::1") for item in data["qualified_node_ids"])


@pytest.mark.asyncio
async def test_workflow_save_rejects_bad_json(monkeypatch):
    monkeypatch.setattr(workflows_routes, "_csrf_error", lambda request: None)
    monkeypatch.setattr(workflows_routes, "_require_write_access", lambda request: Result.Ok(True))

    async def _bad_json(_request):
        return Result.Err("INVALID_JSON", "Invalid JSON body")

    monkeypatch.setattr(workflows_routes, "_read_json", _bad_json)

    app = _build_app()
    req = make_mocked_request("POST", "/mjr/am/workflows/save", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "INVALID_JSON"


@pytest.mark.asyncio
async def test_workflow_save_forwards_info_fields(monkeypatch):
    monkeypatch.setattr(workflows_routes, "_csrf_error", lambda request: None)
    monkeypatch.setattr(workflows_routes, "_require_write_access", lambda request: Result.Ok(True))

    async def _json(_request):
        return Result.Ok(
            {
                "workflow": {"nodes": []},
                "name": "demo",
                "task": "I2V",
                "model_family": "Wan",
                "provider": "local",
                "runs_on": "local",
                "notes": "Keep camera path locked.",
            }
        )

    captured = {}

    def _save_workflow(**kwargs):
        captured.update(kwargs)
        return Result.Ok({"saved": True, "workflow": {"filename": "demo.json"}})

    async def _audit(*args, **kwargs):
        return None

    monkeypatch.setattr(workflows_routes, "_read_json", _json)
    monkeypatch.setattr(workflows_routes, "save_workflow", _save_workflow)
    monkeypatch.setattr(workflows_routes, "_audit_workflow_write", _audit)

    app = _build_app()
    req = make_mocked_request("POST", "/mjr/am/workflows/save", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is True
    assert captured["name"] == "demo"
    assert captured["category"] == "I2V"
    assert captured["info"] == {
        "task": "I2V",
        "model_family": "Wan",
        "provider": "local",
        "runs_on": "local",
        "notes": "Keep camera path locked.",
    }


@pytest.mark.asyncio
async def test_workflow_thumbnail_missing_returns_not_found(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    workflow_dir.mkdir()
    missing = workflow_dir / "missing.png"

    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))
    monkeypatch.setattr(workflows_routes, "is_workflow_thumbnail_path", lambda _path: True)

    app = _build_app()
    req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/thumbnail?filepath={missing.as_posix()}",
        app=app,
    )
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "NOT_FOUND"


@pytest.mark.asyncio
async def test_workflow_graph_map_thumbnail_returns_svg(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    workflow_dir.mkdir()
    workflow_path = workflow_dir / "graph-map.json"
    workflow_path.write_text(json.dumps({"name": "Graph Map", "nodes": [{"id": 1, "type": "KSampler"}]}), encoding="utf-8")
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))

    app = _build_app()
    req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/graph-map-thumbnail?filepath={workflow_path.as_posix()}",
        app=app,
    )
    match = await app.router.resolve(req)
    resp = await match.handler(req)

    assert resp.content_type == "image/svg+xml"
    assert "<svg" in resp.text
    assert resp.headers.get("ETag")
    assert resp.headers.get("Cache-Control") == "private, max-age=300"


@pytest.mark.asyncio
async def test_workflow_graph_map_thumbnail_supports_conditional_cache(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    workflow_dir.mkdir()
    workflow_path = workflow_dir / "graph-map.json"
    workflow_path.write_text(json.dumps({"name": "Graph Map", "nodes": [{"id": 1, "type": "KSampler"}]}), encoding="utf-8")
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))

    app = _build_app()
    first_req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/graph-map-thumbnail?filepath={workflow_path.as_posix()}",
        app=app,
    )
    first_match = await app.router.resolve(first_req)
    first_resp = await first_match.handler(first_req)
    etag = first_resp.headers.get("ETag")
    assert etag

    cached_req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/graph-map-thumbnail?filepath={workflow_path.as_posix()}",
        headers={"If-None-Match": etag},
        app=app,
    )
    cached_match = await app.router.resolve(cached_req)
    cached_resp = await cached_match.handler(cached_req)

    assert cached_resp.status == 304
    assert cached_resp.headers.get("ETag") == etag
    assert cached_resp.headers.get("Cache-Control") == "private, max-age=300"


@pytest.mark.asyncio
async def test_workflow_graph_map_thumbnail_supports_if_modified_since(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    workflow_dir.mkdir()
    workflow_path = workflow_dir / "graph-map.json"
    workflow_path.write_text(
        json.dumps({"name": "Graph Map", "nodes": [{"id": 1, "type": "KSampler"}]}),
        encoding="utf-8",
    )
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))

    app = _build_app()
    first_req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/graph-map-thumbnail?filepath={workflow_path.as_posix()}",
        app=app,
    )
    first_match = await app.router.resolve(first_req)
    first_resp = await first_match.handler(first_req)
    last_modified = first_resp.headers.get("Last-Modified")
    assert last_modified

    cached_req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/graph-map-thumbnail?filepath={workflow_path.as_posix()}",
        headers={"If-Modified-Since": last_modified},
        app=app,
    )
    cached_match = await app.router.resolve(cached_req)
    cached_resp = await cached_match.handler(cached_req)

    assert cached_resp.status == 304
    assert cached_resp.headers.get("Last-Modified") == last_modified
    assert cached_resp.headers.get("Cache-Control") == "private, max-age=300"


@pytest.mark.asyncio
async def test_workflow_model_families_returns_indexed_values(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    workflow_dir.mkdir()
    (workflow_dir / "flux.json").write_text(
        json.dumps({"name": "Flux Demo", "model_family": "Flux", "nodes": [{"id": 1, "type": "KSampler"}]}),
        encoding="utf-8",
    )
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))
    monkeypatch.setattr(workflows_routes, "list_workflow_model_families", lambda: Result.Ok({"model_families": [{"label": "Flux", "value": "Flux", "count": 1}]}))

    app = _build_app()
    req = make_mocked_request("GET", "/mjr/am/workflows/model-families", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is True
    families = body.get("data", {}).get("model_families", [])
    assert families and families[0]["value"] == "Flux"


@pytest.mark.asyncio
async def test_workflow_graph_map_thumbnail_rejects_outside_path(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    workflow_dir.mkdir()
    outside = tmp_path / "outside.json"
    outside.write_text(json.dumps({"name": "Outside", "nodes": []}), encoding="utf-8")
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))

    app = _build_app()
    req = make_mocked_request(
        "GET",
        f"/mjr/am/workflows/graph-map-thumbnail?filepath={outside.as_posix()}",
        app=app,
    )
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "FORBIDDEN"


@pytest.mark.asyncio
async def test_workflow_write_remote_denial(monkeypatch):
    monkeypatch.setattr(workflows_routes, "_csrf_error", lambda request: None)
    monkeypatch.setattr(
        workflows_routes,
        "_require_write_access",
        lambda request: Result.Err("AUTH_REQUIRED", "Write access denied"),
    )

    app = _build_app()
    req = make_mocked_request("POST", "/mjr/am/workflows/delete", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "AUTH_REQUIRED"


@pytest.mark.asyncio
async def test_workflow_favorite_rejects_missing_filepath(monkeypatch):
    monkeypatch.setattr(workflows_routes, "_csrf_error", lambda request: None)
    monkeypatch.setattr(workflows_routes, "_require_write_access", lambda request: Result.Ok(True))

    async def _json_without_filepath(_request):
        return Result.Ok({"favorite": True})

    monkeypatch.setattr(workflows_routes, "_read_json", _json_without_filepath)

    app = _build_app()
    req = make_mocked_request("POST", "/mjr/am/workflows/favorite", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "INVALID_INPUT"


@pytest.mark.asyncio
async def test_workflow_tags_rejects_missing_filepath(monkeypatch):
    monkeypatch.setattr(workflows_routes, "_csrf_error", lambda request: None)
    monkeypatch.setattr(workflows_routes, "_require_write_access", lambda request: Result.Ok(True))

    async def _json_without_filepath(_request):
        return Result.Ok({"tags": ["cinematic"]})

    monkeypatch.setattr(workflows_routes, "_read_json", _json_without_filepath)

    app = _build_app()
    req = make_mocked_request("POST", "/mjr/am/workflows/tags", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "INVALID_INPUT"


@pytest.mark.asyncio
async def test_workflow_tags_accepts_valid_payload(monkeypatch, tmp_path):
    workflow_dir = tmp_path / "workflows"
    index_db = tmp_path / "index.sqlite"
    workflow_dir.mkdir()
    workflow_path = workflow_dir / "taggable.json"
    original = json.dumps({"name": "Taggable", "nodes": [{"id": 1, "type": "KSampler"}]})
    workflow_path.write_text(original, encoding="utf-8")
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", str(workflow_dir))
    monkeypatch.setattr(workflows_service, "get_runtime_index_db_path", lambda: index_db)
    monkeypatch.setattr(workflows_routes, "_csrf_error", lambda request: None)
    monkeypatch.setattr(workflows_routes, "_require_write_access", lambda request: Result.Ok(True))

    async def _json_with_tags(_request):
        return Result.Ok({"filepath": str(workflow_path), "tags": ["cinematic", "flux"]})

    monkeypatch.setattr(workflows_routes, "_read_json", _json_with_tags)

    app = _build_app()
    req = make_mocked_request("POST", "/mjr/am/workflows/tags", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is True
    assert body.get("data", {}).get("tags") == ["cinematic", "flux"]
    assert workflow_path.read_text(encoding="utf-8") == original


@pytest.mark.asyncio
async def test_workflow_thumbnail_candidates_rejects_missing_filepath(monkeypatch):
    app = _build_app()
    req = make_mocked_request("GET", "/mjr/am/workflows/thumbnail-candidates", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "INVALID_INPUT"


@pytest.mark.asyncio
async def test_workflow_thumbnail_rejects_missing_source(monkeypatch):
    workflow_dir = tempfile.mkdtemp()
    monkeypatch.setenv("MJR_AM_WORKFLOW_DIRECTORY", workflow_dir)
    monkeypatch.setattr(workflows_routes, "_csrf_error", lambda request: None)
    monkeypatch.setattr(workflows_routes, "_require_write_access", lambda request: Result.Ok(True))

    workflow_path = f"{workflow_dir}/demo.json"
    with open(workflow_path, "w", encoding="utf-8") as handle:
        json.dump({"name": "Demo", "nodes": []}, handle)

    async def _json_without_source(_request):
        return Result.Ok({"filepath": workflow_path})

    monkeypatch.setattr(workflows_routes, "_read_json", _json_without_source)

    app = _build_app()
    req = make_mocked_request("POST", "/mjr/am/workflows/thumbnail/set", app=app)
    match = await app.router.resolve(req)
    resp = await match.handler(req)
    body = json.loads(resp.text)

    assert body.get("ok") is False
    assert body.get("code") == "INVALID_INPUT"
