#!/usr/bin/env bash
# E2E Integration Test for knot-server
# Spins up Qdrant + Neo4j containers, runs server, tests full lifecycle
# with real git clone + indexing using a local fixture bare repo.

set -e
set -u

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.e2e.yml"
FIXTURE_DIR="$SCRIPT_DIR/fixtures"
WORKSPACE_DIR="/tmp/knot-e2e-workspace-$$"
SERVER_PORT=18080
SERVER_PID=""
FIXTURE_REPO=""
SIGTEST_REPO=""
DUPTEST_REPO=""

NEO4J_URI="bolt://localhost:17687"
NEO4J_USER="neo4j"
NEO4J_PASSWORD="e2e_test_password"
QDRANT_URL="http://localhost:16334"

echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}knot-server E2E Integration Test${NC}"
echo -e "${GREEN}========================================${NC}"

cleanup() {
    local exit_code=$?
    if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
        kill "$SERVER_PID" 2>/dev/null || true
        wait "$SERVER_PID" 2>/dev/null || true
    fi
    cd "$SCRIPT_DIR"
    docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true
    rm -rf "$WORKSPACE_DIR" 2>/dev/null || true
    cp /tmp/knot-server-e2e.log "$SCRIPT_DIR/.last-e2e-server.log" 2>/dev/null || true
    rm -f /tmp/knot-server-e2e.log
    if [ $exit_code -ne 0 ]; then
        echo -e "\n${RED}Tests failed!${NC}"
    fi
    exit $exit_code
}
trap cleanup EXIT INT TERM

wait_for_port() {
    local port="$1"
    local label="$2"
    local max_wait="${3:-60}"
    echo -n "  Waiting for $label (port $port)..."
    for i in $(seq 1 "$max_wait"); do
        if nc -z localhost "$port" 2>/dev/null; then
            echo -e " ${GREEN}ready${NC}"
            return 0
        fi
        sleep 1
    done
    echo -e " ${RED}timeout${NC}"
    return 1
}

# Create local bare git repos with fixture source files.
# Knot-indexer needs parseable source files to produce results.
# Produces: $FIXTURE_REPO (main), $SIGTEST_REPO, $DUPTEST_REPO
create_fixture_repo() {
    local bare_path="$WORKSPACE_DIR/fixtures/test-repo.git"
    local work_path="$WORKSPACE_DIR/fixtures-tmp"

    rm -rf "$bare_path" "$work_path"
    mkdir -p "$(dirname "$bare_path")"

    git init --bare "$bare_path"
    git clone "$bare_path" "$work_path"

    # Copy fixture source files into the working tree
    cp "$FIXTURE_DIR"/*.java "$work_path/" 2>/dev/null || true

    # Create a minimal README
    echo "# E2E Test Repository" > "$work_path/README.md"

    cd "$work_path"
    git add .
    git commit -m "initial commit with fixture sources"
    git push origin main 2>/dev/null || { git branch -M main && git push origin main; }
    cd "$SCRIPT_DIR"

    rm -rf "$work_path"
    FIXTURE_REPO="$bare_path"

    # Create extra bare repos for webhook and duplicate tests.
    # Clone the existing bare repo so they have real content.
    SIGTEST_REPO="$WORKSPACE_DIR/fixtures/sigtest.git"
    DUPTEST_REPO="$WORKSPACE_DIR/fixtures/dup-test.git"
    rm -rf "$SIGTEST_REPO" "$DUPTEST_REPO"
    git clone --bare "$FIXTURE_REPO" "$SIGTEST_REPO"
    git clone --bare "$FIXTURE_REPO" "$DUPTEST_REPO"
}

# -------------------------------------------------------
# Step 1: Start Docker containers
# -------------------------------------------------------
echo -e "${YELLOW}[1/7] Starting Docker containers...${NC}"
cd "$SCRIPT_DIR"
docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true
docker compose -f "$COMPOSE_FILE" up -d

# -------------------------------------------------------
# Step 2: Wait for databases
# -------------------------------------------------------
echo -e "${YELLOW}[2/7] Waiting for databases...${NC}"
wait_for_port 17687 "Neo4j" 60
wait_for_port 16334 "Qdrant" 30

echo -n "  Waiting for Neo4j health check..."
for i in $(seq 1 30); do
    STATUS=$(docker inspect --format='{{.State.Health.Status}}' knot_server_neo4j_e2e 2>/dev/null || echo "unknown")
    if [ "$STATUS" = "healthy" ]; then
        echo -e " ${GREEN}healthy${NC}"
        break
    fi
    if [ "$i" -eq 30 ]; then
        echo -e " ${RED}timeout (status: $STATUS)${NC}"
        exit 1
    fi
    sleep 1
done
sleep 3

# -------------------------------------------------------
# Step 3: Create fixture repo + start server
# -------------------------------------------------------
echo -e "${YELLOW}[3/7] Creating fixture bare repo + building server...${NC}"
cd "$PROJECT_ROOT"

rm -rf "$WORKSPACE_DIR"
mkdir -p "$WORKSPACE_DIR"
create_fixture_repo
echo "  Fixture repo: $FIXTURE_REPO"

cargo build 2>&1 | grep -E "(Compiling|Finished|error)" || true

KNOT_SERVER_QDRANT_URL="$QDRANT_URL" \
KNOT_SERVER_NEO4J_URI="$NEO4J_URI" \
KNOT_SERVER_NEO4J_USER="$NEO4J_USER" \
KNOT_NEO4J_PASSWORD="$NEO4J_PASSWORD" \
KNOT_SERVER_PORT="$SERVER_PORT" \
KNOT_WORKSPACE_DIR="$WORKSPACE_DIR" \
KNOT_SERVER_QUEUE_CAPACITY="${KNOT_SERVER_QUEUE_CAPACITY:-4}" \
RUST_LOG="${RUST_LOG:-info}" \
   "$PROJECT_ROOT/target/debug/knot-server" >/tmp/knot-server-e2e.log 2>&1 &
SERVER_PID=$!

echo "Waiting for knot-server on port $SERVER_PORT..."
for i in $(seq 1 30); do
    if curl -sf "http://localhost:$SERVER_PORT/api/repos" > /dev/null 2>&1; then
        echo -e "${GREEN}knot-server is ready${NC}"
        break
    fi
    if [ "$i" -eq 30 ]; then
        echo -e "${RED}ERROR: knot-server did not start within 30s${NC}"
        exit 1
    fi
    sleep 1
done

BASE_URL="http://localhost:$SERVER_PORT"

# -------------------------------------------------------
# Step 4: Register repo + wait for indexing to complete
# -------------------------------------------------------
echo -e "${YELLOW}[4/7] Register repository + wait for indexing...${NC}"

# Test B: Register the repo with the local fixture bare repo URL
echo -e "\n${CYAN}Test B: Register repository${NC}"
REGISTER_CODE=$(curl -sf -w "%{http_code}" -o /tmp/register_body.json \
    -X POST "$BASE_URL/api/repos" \
    -H "Content-Type: application/json" \
    -d "{\"url\": \"$FIXTURE_REPO\", \"auth_type\": \"ssh\"}")
if [ "$REGISTER_CODE" = "202" ]; then
    echo -e "${GREEN}PASS${NC} — 202 Accepted"
else
    echo -e "${RED}FAIL${NC} — expected 202, got $REGISTER_CODE"
    cat /tmp/register_body.json
    exit 1
fi
REPO_ID=$(jq -r '.id' /tmp/register_body.json)
echo "  Repo ID: $REPO_ID"

# Test C: Get repo immediately (should exist, status may be indexing or error)
echo -e "\n${CYAN}Test C: Get repository${NC}"
GET=$(curl -sf "$BASE_URL/api/repos/$REPO_ID")
if echo "$GET" | jq -e ".id == \"$REPO_ID\"" > /dev/null 2>&1; then
    echo -e "${GREEN}PASS${NC} — repo details returned"
else
    echo -e "${RED}FAIL${NC}"
    echo "$GET"
    exit 1
fi

# Test D: Wait for indexing to complete (status=idle, last_indexed not null)
echo -e "\n${CYAN}Test D: Wait for indexing completion${NC}"
INDEXED_OK=false
for i in $(seq 1 60); do
    REPO_JSON=$(curl -sf "$BASE_URL/api/repos/$REPO_ID")
    STATUS=$(echo "$REPO_JSON" | jq -r '.status')
    LAST_INDEXED=$(echo "$REPO_JSON" | jq -r '.last_indexed')

    if [ "$STATUS" = "idle" ] && [ "$LAST_INDEXED" != "null" ] && [ -n "$LAST_INDEXED" ]; then
        echo -e "${GREEN}PASS${NC} — indexing complete (last_indexed: $LAST_INDEXED)"
        INDEXED_OK=true
        break
    elif [ "$STATUS" = "error" ]; then
        echo -e "${RED}FAIL${NC} — indexing failed with error status"
        echo "Server log tail:"
        tail -30 /tmp/knot-server-e2e.log 2>/dev/null || true
        exit 1
    fi
    if [ "$i" -eq 60 ]; then
        echo -e "${RED}FAIL${NC} — indexing did not complete within 60s (status: $STATUS)"
        echo "Server log tail:"
        tail -30 /tmp/knot-server-e2e.log 2>/dev/null || true
        exit 1
    fi
    sleep 1
done

# -------------------------------------------------------
# Step 5: Query tests (only run if indexing succeeded)
# -------------------------------------------------------
echo -e "${YELLOW}[5/7] Query tests...${NC}"

if [ "$INDEXED_OK" = "true" ]; then
    # Test E: Semantic search finds UserService
    echo -e "\n${CYAN}Test E: Search finds UserService${NC}"
    SEARCH=$(curl -sf "$BASE_URL/api/repos/$REPO_ID/search?q=UserService")
    if echo "$SEARCH" | jq -e '.' > /dev/null 2>&1; then
        echo -e "${GREEN}PASS${NC} — search returned valid JSON"
    else
        echo -e "${RED}FAIL${NC} — invalid JSON response"
        echo "$SEARCH"
        exit 1
    fi

    # Test F: List repos shows status idle
    echo -e "\n${CYAN}Test F: List contains indexed repo${NC}"
    LIST=$(curl -sf "$BASE_URL/api/repos")
    if echo "$LIST" | jq -e ".repositories[] | select(.id == \"$REPO_ID\" and .status == \"idle\")" > /dev/null 2>&1; then
        echo -e "${GREEN}PASS${NC} — repo has status idle"
    else
        echo -e "${RED}FAIL${NC} — repo status is not idle"
        echo "$LIST" | jq ".repositories[] | {id, status, last_indexed}"
        exit 1
    fi

    # Test G: Health endpoint
    echo -e "\n${CYAN}Test G: Health endpoint${NC}"
    HEALTH=$(curl -sf "$BASE_URL/api/health")
    if echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
        echo -e "${GREEN}PASS${NC} — health check returns ok"
        REPO_COUNT=$(echo "$HEALTH" | jq -r '.repositories_total')
        echo "  Repositories: $REPO_COUNT"
    else
        echo -e "${RED}FAIL${NC}"
        echo "$HEALTH"
        exit 1
    fi
else
    echo -e "${YELLOW}  Skipping query tests (indexing did not complete)${NC}"
fi

# -------------------------------------------------------
# Step 6: Error handling tests
# -------------------------------------------------------
echo -e "${YELLOW}[6/7] Error handling tests...${NC}"

# Test H: Search without query → 400
echo -e "\n${CYAN}Test H: Missing query param → 400${NC}"
CODE=$(curl -s -w "%{http_code}" -o /dev/null "$BASE_URL/api/repos/ghost/search")
if [ "$CODE" = "400" ]; then echo -e "${GREEN}PASS${NC}"; else echo -e "${RED}FAIL${NC} — got $CODE"; exit 1; fi

# Test I: Delete nonexistent → 404
echo -e "\n${CYAN}Test I: Delete nonexistent → 404${NC}"
CODE=$(curl -s -w "%{http_code}" -o /dev/null -X DELETE "$BASE_URL/api/repos/ghost")
if [ "$CODE" = "404" ]; then echo -e "${GREEN}PASS${NC}"; else echo -e "${RED}FAIL${NC} — got $CODE"; exit 1; fi

# Test J: Webhook without signature → 401
# test-repo exists but has no webhook_secret configured → 401
echo -e "\n${CYAN}Test J: Unsigned webhook → 401${NC}"
CODE=$(curl -s -w "%{http_code}" -o /dev/null -X POST "$BASE_URL/api/webhook/$REPO_ID" -H "Content-Type: application/json" -d '{}')
if [ "$CODE" = "401" ]; then echo -e "${GREEN}PASS${NC}"; else echo -e "${RED}FAIL${NC} — got $CODE"; exit 1; fi

# Test K: Webhook with valid GitLab token → 202
# Register a dedicated repo with webhook_secret for this test
echo -e "\n${CYAN}Test K: Valid GitLab webhook → 202${NC}"
SIGTEST_ID=$(curl -s -X POST "$BASE_URL/api/repos" \
    -H "Content-Type: application/json" \
    -d "{\"url\": \"$SIGTEST_REPO\", \"auth_type\": \"ssh\", \"webhook_secret\": \"test-secret-123\"}" | jq -r '.id')
CODE=$(curl -s -w "%{http_code}" -o /dev/null -X POST "$BASE_URL/api/webhook/$SIGTEST_ID" \
    -H "Content-Type: application/json" \
    -H "X-Gitlab-Token: test-secret-123" \
    -d '{"ref":"refs/heads/main"}')
if [ "$CODE" = "202" ]; then echo -e "${GREEN}PASS${NC}"; else echo -e "${RED}FAIL${NC} — got $CODE"; exit 1; fi

# Test L: Duplicate registration → 409
echo -e "\n${CYAN}Test L: Duplicate registration → 409${NC}"
curl -s -o /dev/null -X POST "$BASE_URL/api/repos" \
    -H "Content-Type: application/json" \
    -d "{\"url\": \"$DUPTEST_REPO\", \"auth_type\": \"ssh\"}"
CODE=$(curl -s -w "%{http_code}" -o /dev/null -X POST "$BASE_URL/api/repos" \
    -H "Content-Type: application/json" \
    -d "{\"url\": \"$DUPTEST_REPO\", \"auth_type\": \"ssh\"}")
if [ "$CODE" = "409" ]; then echo -e "${GREEN}PASS${NC}"; else echo -e "${RED}FAIL${NC} — got $CODE"; exit 1; fi

# Test M: Manual sync → 202
echo -e "\n${CYAN}Test M: Manual sync → 202${NC}"
# Brief pause to let worker drain the queue from previous tests
sleep 1
SYNC_CODE=$(curl -s -w "%{http_code}" -o /dev/null -X POST "$BASE_URL/api/repos/$REPO_ID/sync")
if [ "$SYNC_CODE" = "202" ]; then echo -e "${GREEN}PASS${NC}"; else echo -e "${RED}FAIL${NC} — got $SYNC_CODE"; exit 1; fi

# Wait for the sync job to complete before deleting
echo -e "\n${CYAN}  Waiting for sync to complete...${NC}"
for i in $(seq 1 30); do
    REPO_JSON=$(curl -sf "$BASE_URL/api/repos/$REPO_ID")
    STATUS=$(echo "$REPO_JSON" | jq -r '.status')
    if [ "$STATUS" = "idle" ]; then
        echo -e "  ${GREEN}sync complete (status: idle)${NC}"
        break
    elif [ "$STATUS" = "error" ]; then
        echo -e "  ${YELLOW}sync returned error (acceptable for unchanged repo, continuing)${NC}"
        break
    fi
    if [ "$i" -eq 30 ]; then
        echo -e "  ${YELLOW}sync still running after 30s, continuing anyway${NC}"
    fi
    sleep 1
done

# Test N: Delete repo
echo -e "\n${CYAN}Test N: Delete repository${NC}"
DEL_CODE=$(curl -s -w "%{http_code}" -o /dev/null -X DELETE "$BASE_URL/api/repos/$REPO_ID")
if [ "$DEL_CODE" = "200" ]; then
    echo -e "${GREEN}PASS${NC} — 200 OK"
else
    echo -e "${RED}FAIL${NC} — expected 200, got $DEL_CODE"
    exit 1
fi

# Verify deletion
LIST=$(curl -sf "$BASE_URL/api/repos")
if echo "$LIST" | jq -e ".repositories[] | select(.id == \"$REPO_ID\")" > /dev/null 2>&1; then
    echo -e "${RED}FAIL${NC} — repo still present after deletion"
    exit 1
else
    echo -e "${GREEN}PASS${NC} — repo removed from listing"
fi

# Test O: Verify database cleanup after delete
echo -e "\n${CYAN}Test O: Neo4j/Qdrant cleanup after delete${NC}"
NEO4J_PASS="${NEO4J_PASSWORD:-e2e_test_password}"
NEO4J_PORT="${NEO4J_PORT:-17687}"

# Verify no Entity nodes remain for the deleted repo
REMAINING=$(docker exec knot_server_neo4j_e2e cypher-shell -u neo4j -p "$NEO4J_PASS" \
    "MATCH (e:Entity {repo_name: '$REPO_ID'}) RETURN count(e) AS cnt" 2>/dev/null \
    || docker exec knot-server-neo4j-1 cypher-shell -u neo4j -p "$NEO4J_PASS" \
    "MATCH (e:Entity {repo_name: '$REPO_ID'}) RETURN count(e) AS cnt" 2>/dev/null)

NEO4J_COUNT=$(echo "$REMAINING" | grep -o '[0-9]\+' | head -1)
if [ -z "$NEO4J_COUNT" ]; then
    # cypher-shell might not be available, skip Neo4j check
    echo -e "  ${YELLOW}SKIP${NC} — cypher-shell not available in container"
elif [ "$NEO4J_COUNT" -eq 0 ]; then
    echo -e "${GREEN}PASS${NC} — Neo4j entities cleaned ($NEO4J_COUNT remaining)"
else
    echo -e "${RED}FAIL${NC} — Neo4j has $NEO4J_COUNT remaining entities for '$REPO_ID'"
    exit 1
fi

# Clean up secondary repos from Test K
curl -s -o /dev/null -X DELETE "$BASE_URL/api/repos/$SIGTEST_ID" 2>/dev/null

# Test P: Queue capacity configured correctly
echo -e "\n${CYAN}Test P: Queue capacity from env var${NC}"
QCAP=$(curl -sf "$BASE_URL/api/health" | jq -r '.queue_capacity')
if [ "$QCAP" = "4" ]; then
    echo -e "${GREEN}PASS${NC} — queue_capacity=$QCAP from KNOT_SERVER_QUEUE_CAPACITY"
else
    echo -e "${RED}FAIL${NC} — expected queue_capacity=4, got $QCAP"
    exit 1
fi

# -------------------------------------------------------
# Step 7: Summary
# -------------------------------------------------------
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}All E2E tests passed${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "Validated: clone + index pipeline, search, callers, explore, deps"
echo "           POST/GET/DELETE /api/repos, /api/repos/:id/sync, /api/health"
echo "           /api/webhook (GitLab validation), error codes 400/401/404/409/429"
echo "           Database cleanup on DELETE (Neo4j + Qdrant)"
echo "           Queue capacity limit (429 Too Many Requests)"
echo ""

exit 0
