from bs4 import BeautifulSoup as Soup
import json
import pytest
import re
from .fixtures import make_app_client, app_client


@pytest.fixture
def canned_write_client(tmpdir):
    template_dir = tmpdir / "canned_write_templates"
    template_dir.mkdir()
    (template_dir / "query-data-update_name.html").write_text(
        """
    {% extends "query.html" %}
    {% block content %}!!!CUSTOM_UPDATE_NAME_TEMPLATE!!!{{ super() }}{% endblock %}
    """,
        "utf-8",
    )
    with make_app_client(
        extra_databases={"data.db": "create table names (name text)"},
        template_dir=str(template_dir),
        config={
            "databases": {
                "data": {
                    "queries": {
                        "canned_read": {"sql": "select * from names"},
                        "add_name": {
                            "sql": "insert into names (name) values (:name)",
                            "write": True,
                            "on_success_redirect": "/data/add_name?success",
                        },
                        "add_name_specify_id": {
                            "sql": "insert into names (rowid, name) values (:rowid, :name)",
                            "on_success_message_sql": "select 'Name added: ' || :name || ' with rowid ' || :rowid",
                            "write": True,
                            "on_error_redirect": "/data/add_name_specify_id?error",
                        },
                        "add_name_specify_id_with_error_in_on_success_message_sql": {
                            "sql": "insert into names (rowid, name) values (:rowid, :name)",
                            "on_success_message_sql": "select this is bad SQL",
                            "write": True,
                        },
                        "delete_name": {
                            "sql": "delete from names where rowid = :rowid",
                            "write": True,
                            "on_success_message": "Name deleted",
                            "allow": {"id": "root"},
                        },
                        "update_name": {
                            "sql": "update names set name = :name where rowid = :rowid",
                            "params": ["rowid", "name", "extra"],
                            "write": True,
                        },
                    }
                }
            }
        },
    ) as client:
        yield client


@pytest.fixture
def canned_write_immutable_client():
    with make_app_client(
        is_immutable=True,
        config={
            "databases": {
                "fixtures": {
                    "queries": {
                        "add": {
                            "sql": "insert into sortable (text) values (:text)",
                            "write": True,
                        },
                    }
                }
            }
        },
    ) as client:
        yield client


@pytest.mark.asyncio
async def test_canned_query_with_named_parameter(ds_client):
    response = await ds_client.get(
        "/fixtures/neighborhood_search.json?text=town&_shape=arrays"
    )
    assert response.json()["rows"] == [
        ["Corktown", "Detroit", "MI"],
        ["Downtown", "Los Angeles", "CA"],
        ["Downtown", "Detroit", "MI"],
        ["Greektown", "Detroit", "MI"],
        ["Koreatown", "Los Angeles", "CA"],
        ["Mexicantown", "Detroit", "MI"],
    ]


def test_insert(canned_write_client):
    response = canned_write_client.post(
        "/data/add_name",
        {"name": "Hello"},
        csrftoken_from=True,
        cookies={"foo": "bar"},
    )
    messages = canned_write_client.ds.unsign(
        response.cookies["ds_messages"], "messages"
    )
    assert messages == [["Query executed, 1 row affected", 1]]
    assert response.status == 302
    assert response.headers["Location"] == "/data/add_name?success"


@pytest.mark.parametrize(
    "query_name,expect_csrf_hidden_field",
    [
        ("canned_read", False),
        ("add_name_specify_id", True),
        ("add_name", True),
    ],
)
def test_canned_query_form_csrf_hidden_field(
    canned_write_client, query_name, expect_csrf_hidden_field
):
    response = canned_write_client.get(f"/data/{query_name}")
    html = response.text
    fragment = '<input type="hidden" name="csrftoken" value="'
    if expect_csrf_hidden_field:
        assert fragment in html
    else:
        assert fragment not in html


def test_insert_with_cookies_requires_csrf(canned_write_client):
    response = canned_write_client.post(
        "/data/add_name",
        {"name": "Hello"},
        cookies={"foo": "bar"},
    )
    assert 403 == response.status


def test_insert_no_cookies_no_csrf(canned_write_client):
    response = canned_write_client.post("/data/add_name", {"name": "Hello"})
    assert 302 == response.status
    assert "/data/add_name?success" == response.headers["Location"]


def test_custom_success_message(canned_write_client):
    response = canned_write_client.post(
        "/data/delete_name",
        {"rowid": 1},
        cookies={"ds_actor": canned_write_client.actor_cookie({"id": "root"})},
        csrftoken_from=True,
    )
    assert 302 == response.status
    messages = canned_write_client.ds.unsign(
        response.cookies["ds_messages"], "messages"
    )
    assert [["Name deleted", 1]] == messages


def test_insert_error(canned_write_client):
    canned_write_client.post("/data/add_name", {"name": "Hello"}, csrftoken_from=True)
    response = canned_write_client.post(
        "/data/add_name_specify_id",
        {"rowid": 1, "name": "Should fail"},
        csrftoken_from=True,
    )
    assert 302 == response.status
    assert "/data/add_name_specify_id?error" == response.headers["Location"]
    messages = canned_write_client.ds.unsign(
        response.cookies["ds_messages"], "messages"
    )
    assert [["UNIQUE constraint failed: names.rowid", 3]] == messages
    # How about with a custom error message?
    canned_write_client.ds.config["databases"]["data"]["queries"][
        "add_name_specify_id"
    ]["on_error_message"] = "ERROR"
    response = canned_write_client.post(
        "/data/add_name_specify_id",
        {"rowid": 1, "name": "Should fail"},
        csrftoken_from=True,
    )
    assert [["ERROR", 3]] == canned_write_client.ds.unsign(
        response.cookies["ds_messages"], "messages"
    )


def test_on_success_message_sql(canned_write_client):
    response = canned_write_client.post(
        "/data/add_name_specify_id",
        {"rowid": 5, "name": "Should be OK"},
        csrftoken_from=True,
    )
    assert response.status == 302
    assert response.headers["Location"] == "/data/add_name_specify_id"
    messages = canned_write_client.ds.unsign(
        response.cookies["ds_messages"], "messages"
    )
    assert messages == [["Name added: Should be OK with rowid 5", 1]]


def test_error_in_on_success_message_sql(canned_write_client):
    response = canned_write_client.post(
        "/data/add_name_specify_id_with_error_in_on_success_message_sql",
        {"rowid": 1, "name": "Should fail"},
        csrftoken_from=True,
    )
    messages = canned_write_client.ds.unsign(
        response.cookies["ds_messages"], "messages"
    )
    assert messages == [
        ["Error running on_success_message_sql: no such column: bad", 3]
    ]


def test_custom_params(canned_write_client):
    response = canned_write_client.get("/data/update_name?extra=foo")
    assert '<input type="text" id="qp3" name="extra" value="foo">' in response.text


def test_vary_header(canned_write_client):
    # These forms embed a csrftoken so they should be served with Vary: Cookie
    assert "vary" not in canned_write_client.get("/data").headers
    assert "Cookie" == canned_write_client.get("/data/update_name").headers["vary"]


def test_json_post_body(canned_write_client):
    response = canned_write_client.post(
        "/data/add_name",
        body=json.dumps({"name": ["Hello", "there"]}),
    )
    assert 302 == response.status
    assert "/data/add_name?success" == response.headers["Location"]
    rows = canned_write_client.get("/data/names.json?_shape=array").json
    assert rows == [{"rowid": 1, "name": "['Hello', 'there']"}]


@pytest.mark.parametrize(
    "headers,body,querystring",
    (
        (None, "name=NameGoesHere", "?_json=1"),
        ({"Accept": "application/json"}, "name=NameGoesHere", None),
        (None, "name=NameGoesHere&_json=1", None),
        (None, '{"name": "NameGoesHere", "_json": 1}', None),
    ),
)
def test_json_response(canned_write_client, headers, body, querystring):
    response = canned_write_client.post(
        "/data/add_name" + (querystring or ""),
        body=body,
        headers=headers,
    )
    assert 200 == response.status
    assert response.headers["content-type"] == "application/json; charset=utf-8"
    assert response.json == {
        "ok": True,
        "message": "Query executed, 1 row affected",
        "redirect": "/data/add_name?success",
    }
    rows = canned_write_client.get("/data/names.json?_shape=array").json
    assert rows == [{"rowid": 1, "name": "NameGoesHere"}]


def test_canned_query_permissions_on_database_page(canned_write_client):
    # Without auth only shows three queries
    query_names = {
        q["name"] for q in canned_write_client.get("/data.json").json["queries"]
    }
    assert query_names == {
        "add_name_specify_id_with_error_in_on_success_message_sql",
        "from_hook",
        "update_name",
        "add_name_specify_id",
        "from_async_hook",
        "canned_read",
        "add_name",
    }

    # With auth shows four
    response = canned_write_client.get(
        "/data.json",
        cookies={"ds_actor": canned_write_client.actor_cookie({"id": "root"})},
    )
    assert response.status == 200
    query_names_and_private = sorted(
        [
            {"name": q["name"], "private": q["private"]}
            for q in response.json["queries"]
        ],
        key=lambda q: q["name"],
    )
    assert query_names_and_private == [
        {"name": "add_name", "private": False},
        {"name": "add_name_specify_id", "private": False},
        {
            "name": "add_name_specify_id_with_error_in_on_success_message_sql",
            "private": False,
        },
        {"name": "canned_read", "private": False},
        {"name": "delete_name", "private": True},
        {"name": "from_async_hook", "private": False},
        {"name": "from_hook", "private": False},
        {"name": "update_name", "private": False},
    ]


def test_canned_query_permissions(canned_write_client):
    assert 403 == canned_write_client.get("/data/delete_name").status
    assert 200 == canned_write_client.get("/data/update_name").status
    cookies = {"ds_actor": canned_write_client.actor_cookie({"id": "root"})}
    assert 200 == canned_write_client.get("/data/delete_name", cookies=cookies).status
    assert 200 == canned_write_client.get("/data/update_name", cookies=cookies).status


@pytest.fixture(scope="session")
def magic_parameters_client():
    with make_app_client(
        extra_databases={"data.db": "create table logs (line text)"},
        config={
            "databases": {
                "data": {
                    "queries": {
                        "runme_post": {"sql": "", "write": True},
                        "runme_get": {"sql": ""},
                    }
                }
            }
        },
    ) as client:
        yield client


@pytest.mark.parametrize(
    "magic_parameter,expected_re",
    [
        ("_actor_id", "root"),
        ("_header_host", "localhost"),
        ("_header_not_a_thing", ""),
        ("_cookie_foo", "bar"),
        ("_now_epoch", r"^\d+$"),
        ("_now_date_utc", r"^\d{4}-\d{2}-\d{2}$"),
        ("_now_datetime_utc", r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"),
        ("_random_chars_1", r"^\w$"),
        ("_random_chars_10", r"^\w{10}$"),
    ],
)
def test_magic_parameters(magic_parameters_client, magic_parameter, expected_re):
    magic_parameters_client.ds.config["databases"]["data"]["queries"]["runme_post"][
        "sql"
    ] = f"insert into logs (line) values (:{magic_parameter})"
    magic_parameters_client.ds.config["databases"]["data"]["queries"]["runme_get"][
        "sql"
    ] = f"select :{magic_parameter} as result"
    cookies = {
        "ds_actor": magic_parameters_client.actor_cookie({"id": "root"}),
        "foo": "bar",
    }
    # Test the GET version
    get_response = magic_parameters_client.get(
        "/data/runme_get.json?_shape=array", cookies=cookies
    )
    get_actual = get_response.json[0]["result"]
    assert re.match(expected_re, str(get_actual))
    # Test the form
    form_response = magic_parameters_client.get("/data/runme_post")
    soup = Soup(form_response.body, "html.parser")
    # The magic parameter should not be represented as a form field
    assert None is soup.find("input", {"name": magic_parameter})
    # Submit the form to create a log line
    response = magic_parameters_client.post(
        "/data/runme_post?_json=1", {}, csrftoken_from=True, cookies=cookies
    )
    assert response.json == {
        "ok": True,
        "message": "Query executed, 1 row affected",
        "redirect": None,
    }
    post_actual = magic_parameters_client.get(
        "/data/logs.json?_sort_desc=rowid&_shape=array"
    ).json[0]["line"]
    assert re.match(expected_re, post_actual)


@pytest.mark.parametrize("use_csrf", [True, False])
@pytest.mark.parametrize("return_json", [True, False])
def test_magic_parameters_csrf_json(magic_parameters_client, use_csrf, return_json):
    magic_parameters_client.ds.config["databases"]["data"]["queries"]["runme_post"][
        "sql"
    ] = "insert into logs (line) values (:_header_host)"
    qs = ""
    if return_json:
        qs = "?_json=1"
    response = magic_parameters_client.post(
        f"/data/runme_post{qs}",
        {},
        csrftoken_from=use_csrf or None,
    )
    if return_json:
        assert response.status == 200
        assert response.json["ok"], response.json
    else:
        assert response.status == 302
        messages = magic_parameters_client.ds.unsign(
            response.cookies["ds_messages"], "messages"
        )
        assert [["Query executed, 1 row affected", 1]] == messages
    post_actual = magic_parameters_client.get(
        "/data/logs.json?_sort_desc=rowid&_shape=array"
    ).json[0]["line"]
    assert post_actual == "localhost"


def test_magic_parameters_cannot_be_used_in_arbitrary_queries(magic_parameters_client):
    response = magic_parameters_client.get(
        "/data/-/query.json?sql=select+:_header_host&_shape=array"
    )
    assert 400 == response.status
    assert response.json["error"].startswith("You did not supply a value for binding")


def test_canned_write_custom_template(canned_write_client):
    response = canned_write_client.get("/data/update_name")
    assert response.status == 200
    assert "!!!CUSTOM_UPDATE_NAME_TEMPLATE!!!" in response.text
    assert (
        "<!-- Templates considered: *query-data-update_name.html, query-data.html, query.html -->"
        in response.text
    )
    # And test for link rel=alternate while we're here:
    assert (
        '<link rel="alternate" type="application/json+datasette" href="http://localhost/data/update_name.json">'
        in response.text
    )
    assert (
        response.headers["link"]
        == 'http://localhost/data/update_name.json; rel="alternate"; type="application/json+datasette"'
    )


def test_canned_write_query_disabled_for_immutable_database(
    canned_write_immutable_client,
):
    response = canned_write_immutable_client.get("/fixtures/add")
    assert response.status == 200
    assert (
        "This query cannot be executed because the database is immutable."
        in response.text
    )
    assert '<input type="submit" value="Run SQL" disabled>' in response.text
    # Submitting form should get a forbidden error
    response = canned_write_immutable_client.post(
        "/fixtures/add",
        {"text": "text"},
        csrftoken_from=True,
    )
    assert response.status == 403
    assert "Database is immutable" in response.text
