from datasette.app import Datasette
import pytest
import pytest_asyncio
import sqlite3
import tempfile
import pathlib


def create_test_db():
    tmpdir = tempfile.mkdtemp()
    db_path = pathlib.Path(tmpdir) / "test.db"
    conn = sqlite3.connect(str(db_path))
    conn.execute("CREATE TABLE authors (id INTEGER PRIMARY KEY, name TEXT)")
    conn.execute("""
        CREATE TABLE books (
            id INTEGER PRIMARY KEY,
            title TEXT,
            author_id INTEGER REFERENCES authors(id)
        )
        """)
    conn.execute("""
        CREATE TABLE chapters (
            id INTEGER PRIMARY KEY,
            title TEXT,
            book_id INTEGER REFERENCES books(id)
        )
        """)
    # Table with pk column name (reserved in mermaid)
    conn.execute("CREATE TABLE tags (pk TEXT PRIMARY KEY, label TEXT)")
    # Table with reserved word name and column names
    conn.execute('CREATE TABLE "select" ("group" TEXT, "having" TEXT)')
    # Table starting with digit
    conn.execute('CREATE TABLE "123_starts_with_digits" (content TEXT)')
    conn.close()
    return db_path


@pytest_asyncio.fixture
async def ds():
    db_path = create_test_db()
    datasette = Datasette([str(db_path)])
    # Trigger catalog refresh so allowed_resources() works
    await datasette.client.get("/")
    return datasette


@pytest.mark.asyncio
async def test_plugin_is_installed():
    datasette = Datasette(memory=True)
    response = await datasette.client.get("/-/plugins.json")
    assert response.status_code == 200
    installed_plugins = {p["name"] for p in response.json()}
    assert "datasette-table-diagram" in installed_plugins


@pytest.mark.asyncio
async def test_database_action_menu(ds):
    response = await ds.client.get("/test")
    assert response.status_code == 200
    assert "Table diagram" in response.text
    assert "/-/table-diagram/test" in response.text


@pytest.mark.asyncio
async def test_table_diagram_page(ds):
    response = await ds.client.get("/-/table-diagram/test")
    assert response.status_code == 200
    assert "mermaid" in response.text
    assert "erDiagram" in response.text
    # All tables including isolated ones
    assert "authors" in response.text
    assert "books" in response.text
    assert "chapters" in response.text
    assert "tags" in response.text
    # Foreign key relationships
    assert "author_id" in response.text
    assert "book_id" in response.text
    # PK and FK markers
    assert "PK" in response.text
    assert "FK" in response.text


@pytest.mark.asyncio
async def test_table_diagram_not_found():
    datasette = Datasette(memory=True)
    await datasette.client.get("/")
    response = await datasette.client.get("/-/table-diagram/nonexistent")
    assert response.status_code == 404


@pytest.mark.asyncio
async def test_mermaid_text_structure(ds):
    response = await ds.client.get("/-/table-diagram/test")
    text = response.text
    # Entity names quoted, attribute names bare
    assert '"authors" ||--o{ "books" : author_id' in text
    assert '"books" ||--o{ "chapters" : book_id' in text


@pytest.mark.asyncio
async def test_reserved_word_table_names(ds):
    response = await ds.client.get("/-/table-diagram/test")
    text = response.text
    # Reserved word table name should be quoted
    assert '"select"' in text
    # Digit-starting table name should be quoted
    assert '"123_starts_with_digits"' in text
    # Reserved word column names unquoted
    assert "text group" in text
    assert "text having" in text
    # pk column name escaped with underscore suffix
    assert "text pk_ PK" in text


@pytest.mark.asyncio
async def test_view_database_permission_denied():
    """Diagram route returns 403 when view-database is denied."""
    db_path = create_test_db()
    datasette = Datasette(
        [str(db_path)],
        config={"databases": {"test": {"allow": {"id": "allowed"}}}},
    )
    await datasette.client.get("/")
    # Anonymous user should get 403
    response = await datasette.client.get("/-/table-diagram/test")
    assert response.status_code == 403


@pytest.mark.asyncio
async def test_database_action_hidden_without_permission():
    """Action menu item not shown when view-database is denied."""
    db_path = create_test_db()
    datasette = Datasette(
        [str(db_path)],
        config={"databases": {"test": {"allow": {"id": "allowed"}}}},
    )
    await datasette.client.get("/")
    # Allowed user should see the action
    cookies = {"ds_actor": datasette.sign({"a": {"id": "allowed"}}, "actor")}
    allowed_response = await datasette.client.get("/test", cookies=cookies)
    assert allowed_response.status_code == 200
    assert "Table diagram" in allowed_response.text
    # Denied user should not see the database page at all
    cookies2 = {"ds_actor": datasette.sign({"a": {"id": "denied"}}, "actor")}
    denied_response = await datasette.client.get("/test", cookies=cookies2)
    assert denied_response.status_code == 403


@pytest.mark.asyncio
async def test_view_database_permission_granted_with_actor():
    """Diagram route works for actor with view-database permission."""
    db_path = create_test_db()
    datasette = Datasette(
        [str(db_path)],
        config={"databases": {"test": {"allow": {"id": "allowed"}}}},
    )
    await datasette.client.get("/")
    cookies = {"ds_actor": datasette.sign({"a": {"id": "allowed"}}, "actor")}
    response = await datasette.client.get("/-/table-diagram/test", cookies=cookies)
    assert response.status_code == 200
    assert "erDiagram" in response.text


@pytest.mark.asyncio
async def test_only_visible_tables_in_diagram():
    """Only tables the actor has view-table permission for appear."""
    db_path = create_test_db()
    datasette = Datasette(
        [str(db_path)],
        config={
            "databases": {
                "test": {
                    "tables": {
                        "chapters": {"allow": False},
                        "tags": {"allow": False},
                        "select": {"allow": False},
                        "123_starts_with_digits": {"allow": False},
                    }
                }
            }
        },
    )
    await datasette.client.get("/")
    response = await datasette.client.get("/-/table-diagram/test")
    assert response.status_code == 200
    text = response.text
    # Allowed tables should appear
    assert '"authors"' in text
    assert '"books"' in text
    # Denied tables should not appear in the mermaid diagram
    assert '"chapters"' not in text
    assert '"tags"' not in text
    assert '"select"' not in text


@pytest.mark.asyncio
async def test_foreign_keys_filtered_by_table_permissions():
    """FK relationships involving hidden tables are excluded."""
    db_path = create_test_db()
    datasette = Datasette(
        [str(db_path)],
        config={
            "databases": {
                "test": {
                    "tables": {
                        "authors": {"allow": False},
                    }
                }
            }
        },
    )
    await datasette.client.get("/")
    response = await datasette.client.get("/-/table-diagram/test")
    assert response.status_code == 200
    text = response.text
    # books -> chapters FK should be present
    assert '"books" ||--o{ "chapters" : book_id' in text
    # authors -> books FK relationship line should NOT be present
    assert '"authors" ||--o{ "books"' not in text
    # authors table definition should not appear
    assert '"authors" {' not in text
    # author_id column should exist but NOT be marked as FK
    assert "integer author_id FK" not in text
    assert "integer author_id" in text
