from datetime import UTC, datetime

from flask import render_template_string

from l4d2web.app import create_app
from l4d2web.auth import hash_password
from l4d2web.db import init_db, session_scope
from l4d2web.models import Overlay, User


def _make_app(tmp_path, monkeypatch, db_name: str):
    db_url = f"sqlite:///{tmp_path/db_name}"
    monkeypatch.setenv("DATABASE_URL", db_url)
    return create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})


def test_base_layout_is_modal_partial_when_hx_modal_header_set(tmp_path, monkeypatch):
    app = _make_app(tmp_path, monkeypatch, "layout-modal.db")
    with app.test_request_context("/", headers={"HX-Modal": "1"}):
        assert render_template_string("{{ base_layout }}") == "_modal_partial.html"


def test_base_layout_is_base_html_for_normal_request(tmp_path, monkeypatch):
    app = _make_app(tmp_path, monkeypatch, "layout-default.db")
    with app.test_request_context("/"):
        assert render_template_string("{{ base_layout }}") == "base.html"


def test_base_layout_does_not_react_to_plain_hx_request_header(tmp_path, monkeypatch):
    # HTMX sets HX-Request on every request including the build-status poll;
    # only HX-Modal should switch the layout.
    app = _make_app(tmp_path, monkeypatch, "layout-hxreq.db")
    with app.test_request_context("/", headers={"HX-Request": "true"}):
        assert render_template_string("{{ base_layout }}") == "base.html"


def _auth_client_with_files_overlay(tmp_path, monkeypatch, db_name: str):
    db_url = f"sqlite:///{tmp_path/db_name}"
    monkeypatch.setenv("DATABASE_URL", db_url)
    monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
    app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
    init_db()

    with session_scope() as session:
        user = User(username="alice", password_digest=hash_password("secret"), admin=False)
        session.add(user)
        session.flush()
        overlay = Overlay(name="cfgs", path="", type="files", user_id=user.id)
        session.add(overlay)
        session.flush()
        overlay.path = str(overlay.id)
        overlay_root = tmp_path / "overlays" / str(overlay.id)
        overlay_root.mkdir(parents=True)
        (overlay_root / "server.cfg").write_text("hostname \"left4me\"\nrcon_password \"hunter2\"\n", encoding="utf-8")
        user_id = user.id
        overlay_id = overlay.id

    client = app.test_client()
    with client.session_transaction() as sess:
        sess["user_id"] = user_id
        sess["pw_changed_at"] = datetime.now(UTC).isoformat()
    return client, overlay_id


def test_edit_route_renders_full_page_without_modal_header(tmp_path, monkeypatch):
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-full.db")
    response = client.get(f"/overlays/{overlay_id}/files/edit?path=server.cfg")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert "<!doctype html>" in text.lower()  # full base.html rendered
    assert 'href="/dashboard"' in text  # nav present
    assert 'class="files-editor-content"' in text
    assert 'rcon_password' in text  # content pre-filled


def test_edit_route_renders_fragment_with_modal_header(tmp_path, monkeypatch):
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-fragment.db")
    response = client.get(
        f"/overlays/{overlay_id}/files/edit?path=server.cfg",
        headers={"HX-Modal": "1"},
    )
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert "<html" not in text  # layoutless
    assert 'class="primary-nav"' not in text
    assert 'class="files-editor-content"' in text
    assert "hostname" in text  # content pre-filled


def test_edit_route_404s_for_missing_file(tmp_path, monkeypatch):
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-404.db")
    response = client.get(f"/overlays/{overlay_id}/files/edit?path=nonexistent.cfg")
    assert response.status_code == 404


def test_edit_route_renders_binary_template_for_non_editable(tmp_path, monkeypatch):
    """Phase B Step 7: non-editable files no longer return 415; they render
    the editor template with is_binary=True so the modal shows the
    replace-zone UI instead of an error."""
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-binary.db")
    from pathlib import Path
    overlay_root = tmp_path / "overlays" / str(overlay_id)
    Path(overlay_root).joinpath("blob.bin").write_bytes(b"\x00\x01\x02\x03" * 1024)

    response = client.get(f"/overlays/{overlay_id}/files/edit?path=blob.bin")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert 'class="files-editor-binary"' in text
    # Save button is labeled "Replace" and starts disabled (no file queued).
    assert ">Replace</button>" in text
    assert 'class="files-editor-save" disabled' in text
    # Byte count + MIME type rendered.
    assert "4096 bytes" in text  # 4 bytes × 1024
    # The CM6 text editor isn't rendered in binary mode.
    assert 'class="files-editor-text"' not in text


def test_binary_template_has_replace_zone(tmp_path, monkeypatch):
    """Spot-check the binary panel's replace-zone markup: drop zone,
    idle and queued labels, browse button, and a hidden file input."""
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-binary-zone.db")
    from pathlib import Path
    overlay_root = tmp_path / "overlays" / str(overlay_id)
    Path(overlay_root).joinpath("blob.bin").write_bytes(b"\xff" * 100)

    response = client.get(f"/overlays/{overlay_id}/files/edit?path=blob.bin")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert 'class="files-editor-replace-zone"' in text
    assert 'class="files-editor-replace-idle"' in text
    assert 'class="files-editor-replace-queued"' in text
    assert 'class="link-button files-editor-replace-browse"' in text
    assert 'class="files-editor-replace-input" hidden' in text
    # Download link stays visible in binary mode (it's how the user
    # gets the existing file out).
    assert "files-editor-download" in text


def test_edit_route_400s_for_path_traversal(tmp_path, monkeypatch):
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "edit-400.db")
    response = client.get(f"/overlays/{overlay_id}/files/edit?path=../../etc/passwd")
    assert response.status_code == 400


def test_edit_route_404s_for_non_files_overlay(tmp_path, monkeypatch):
    db_url = f"sqlite:///{tmp_path/'edit-script-overlay.db'}"
    monkeypatch.setenv("DATABASE_URL", db_url)
    monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
    app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
    init_db()
    with session_scope() as s:
        user = User(username="alice", password_digest=hash_password("secret"), admin=False)
        s.add(user)
        s.flush()
        overlay = Overlay(name="scripted", path="", type="script", user_id=user.id)
        s.add(overlay)
        s.flush()
        overlay.path = str(overlay.id)
        (tmp_path / "overlays" / str(overlay.id)).mkdir(parents=True)
        user_id = user.id
        overlay_id = overlay.id
    client = app.test_client()
    with client.session_transaction() as sess:
        sess["user_id"] = user_id
        sess["pw_changed_at"] = datetime.now(UTC).isoformat()

    response = client.get(f"/overlays/{overlay_id}/files/edit?path=anything.cfg")
    assert response.status_code == 404


# ---------------------------------------------------------------- /files/new


def test_overlay_detail_no_longer_renders_legacy_editor_dialog(tmp_path, monkeypatch):
    """Phase B Step 9: the inline <dialog id="files-editor-modal"> is gone.
    All editor flows route through the URL-addressable modal instead."""
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "no-legacy-dialog.db")
    response = client.get(f"/overlays/{overlay_id}")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert 'id="files-editor-modal"' not in text
    # The other inline dialogs (new-folder, conflict, delete-confirm) are
    # still inline — only the editor dialog moved out.
    assert 'id="files-new-folder-modal"' in text


def test_new_route_renders_with_empty_content(tmp_path, monkeypatch):
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "new-empty.db")
    response = client.get(f"/overlays/{overlay_id}/files/new")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    # Empty content + empty filename + zero byte count.
    assert 'class="files-editor-content"' in text
    assert 'value=""' in text  # filename input
    assert "UTF-8 · 0 bytes" in text


def test_new_route_renders_with_target_folder_attribute(tmp_path, monkeypatch):
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "new-atfolder.db")
    # Create a subfolder to target.
    (tmp_path / "overlays" / str(overlay_id) / "cfg").mkdir()
    response = client.get(f"/overlays/{overlay_id}/files/new?at=cfg")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    assert 'data-at-folder="cfg"' in text
    # Title text reflects the target folder.
    assert "cfg/…new file" in text


def test_new_route_renders_create_button_not_save(tmp_path, monkeypatch):
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "new-button.db")
    response = client.get(f"/overlays/{overlay_id}/files/new")
    text = response.get_data(as_text=True)

    assert response.status_code == 200
    # Save button shows "Create" instead of "Save"; Delete + Download
    # buttons are absent on a new file.
    assert 'class="files-editor-save">Create</button>' in text
    assert 'class="danger-outline files-editor-delete"' not in text
    assert 'class="button-secondary files-editor-download"' not in text


def test_new_route_400s_for_invalid_at_path(tmp_path, monkeypatch):
    client, overlay_id = _auth_client_with_files_overlay(tmp_path, monkeypatch, "new-400.db")
    response = client.get(f"/overlays/{overlay_id}/files/new?at=../../etc")
    assert response.status_code == 400


def test_new_route_404s_for_non_files_overlay(tmp_path, monkeypatch):
    db_url = f"sqlite:///{tmp_path/'new-script-overlay.db'}"
    monkeypatch.setenv("DATABASE_URL", db_url)
    monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path))
    app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"})
    init_db()
    with session_scope() as s:
        user = User(username="alice", password_digest=hash_password("secret"), admin=False)
        s.add(user)
        s.flush()
        overlay = Overlay(name="scripted", path="", type="script", user_id=user.id)
        s.add(overlay)
        s.flush()
        overlay.path = str(overlay.id)
        (tmp_path / "overlays" / str(overlay.id)).mkdir(parents=True)
        user_id = user.id
        overlay_id = overlay.id
    client = app.test_client()
    with client.session_transaction() as sess:
        sess["user_id"] = user_id
        sess["pw_changed_at"] = datetime.now(UTC).isoformat()

    response = client.get(f"/overlays/{overlay_id}/files/new")
    assert response.status_code == 404
