/**
 * E2E Tests for auth routes with AUTH_ENABLED=true
 */
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from "vitest";
import Fastify, { FastifyInstance } from "fastify";
import cookie from "@fastify/cookie";
import jwt from "@fastify/jwt";
import sensible from "@fastify/sensible";
import { createClient, Client } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";

// Use vi.hoisted to create the db BEFORE mocks are set up
const { testClient, testDb } = vi.hoisted(() => {
  const { createClient } = require("@libsql/client");
  const { drizzle } = require("drizzle-orm/libsql");
  const client = createClient({ url: ":memory:" });
  const db = drizzle(client);
  return { testClient: client, testDb: db };
});

// Mock modules using the hoisted db
vi.mock("../db/client.js", () => ({
  db: testDb,
  migrationsReady: Promise.resolve(),
}));

// Enable auth for these tests
vi.mock("../plugins/env.js", () => ({
  env: {
    AUTH_ENABLED: true,
    LOCAL_AUTH_ENABLED: true,
    REGISTRATION_ENABLED: true,
    OIDC_ENABLED: false,
    NODE_ENV: "test",
    LOG_LEVEL: "silent",
    PORT: 3000,
    CORS_ORIGINS: "*",
    JWT_SECRET: "test-jwt-secret-12345",
    REFRESH_SECRET: "test-refresh-secret-12345",
    COOKIE_SECRET: "test-cookie-secret-12345",
    ACCESS_TOKEN_TTL_MINUTES: 15,
    REFRESH_TOKEN_TTL_DAYS: 7,
  },
}));

// Import real auth plugin and routes
const { authRoutes } = await import("../routes/auth.js");

// =============================================================================
// Test Setup
// =============================================================================

async function createSchema(client: Client) {
  const tableCreations = [
    `CREATE TABLE IF NOT EXISTS users (
      id integer PRIMARY KEY AUTOINCREMENT,
      username text NOT NULL UNIQUE,
      password_hash text,
      avatar_url text,
      auth_provider text NOT NULL DEFAULT 'local',
      oidc_subject text,
      is_active integer NOT NULL DEFAULT 1,
      last_login_at integer,
      created_at integer NOT NULL DEFAULT (strftime('%s','now')),
      updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
    )`,
    `CREATE TABLE IF NOT EXISTS refresh_tokens (
      id integer PRIMARY KEY AUTOINCREMENT,
      user_id integer NOT NULL,
      token_id text NOT NULL UNIQUE,
      expires_at integer NOT NULL,
      revoked integer NOT NULL DEFAULT 0,
      rotated_at integer,
      created_at integer NOT NULL DEFAULT (strftime('%s','now')),
      FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
    )`,
  ];

  for (const sql of tableCreations) {
    await client.execute(sql);
  }
}

async function clearData(client: Client) {
  await client.execute("DELETE FROM refresh_tokens");
  await client.execute("DELETE FROM users");
  await client.execute("DELETE FROM sqlite_sequence");
}

// =============================================================================
// Tests
// =============================================================================

describe("Auth Routes (AUTH_ENABLED=true)", () => {
  let app: FastifyInstance;

  beforeAll(async () => {
    await createSchema(testClient);

    app = Fastify({ logger: false });

    await app.register(sensible);
    await app.register(cookie, { secret: "test-cookie-secret-12345" });
    await app.register(jwt, {
      secret: "test-jwt-secret-12345",
      cookie: { cookieName: "access_token", signed: false },
    });

    // Decorate with config needed by auth routes
    app.decorate("config", {
      accessSecret: "test-jwt-secret-12345",
      refreshSecret: "test-refresh-secret-12345",
      accessTtl: 15,
      refreshTtl: 7,
      cookieOptions: { httpOnly: true, sameSite: "lax", secure: false, path: "/", maxAge: 15 * 60 },
      refreshCookieOptions: { httpOnly: true, sameSite: "lax", secure: false, path: "/auth", maxAge: 7 * 24 * 60 * 60 },
    });

    await app.register(authRoutes);
    await app.ready();
  });

  afterAll(async () => {
    await app.close();
    testClient.close();
  });

  beforeEach(async () => {
    await clearData(testClient);
  });

  // ---------------------------------------------------------------------------
  // Auth State Tests
  // ---------------------------------------------------------------------------

  describe("GET /auth/state", () => {
    it("should return auth state", async () => {
      const response = await app.inject({
        method: "GET",
        url: "/auth/state",
      });

      expect(response.statusCode).toBe(200);
      const data = response.json();
      expect(data.authEnabled).toBe(true);
      expect(data.registrationEnabled).toBe(true);
      expect(data.localAuthEnabled).toBe(true);
    });
  });

  // ---------------------------------------------------------------------------
  // Registration Tests
  // ---------------------------------------------------------------------------

  describe("POST /auth/register", () => {
    it("should register a new user", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "testuser",
          password: "TestPassword123",
        },
      });

      expect(response.statusCode).toBe(201);
      const data = response.json();
      expect(data.ok).toBe(true);
      expect(data.user.username).toBe("testuser");
    });

    it("should reject duplicate username", async () => {
      // First registration
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "duplicate",
          password: "TestPassword123",
        },
      });

      // Second registration with same username
      const response = await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "duplicate",
          password: "AnotherPassword123",
        },
      });

      expect(response.statusCode).toBe(409);
      expect(response.json().code).toBe("USERNAME_EXISTS");
    });

    it("should reject short password", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "testuser",
          password: "short",
        },
      });

      expect(response.statusCode).toBe(400);
      expect(response.json().code).toBe("VALIDATION_ERROR");
    });

    it("should reject short username", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "ab",
          password: "ValidPassword123",
        },
      });

      expect(response.statusCode).toBe(400);
      expect(response.json().code).toBe("VALIDATION_ERROR");
    });

    it("should reject invalid username characters", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "test@user",
          password: "ValidPassword123",
        },
      });

      expect(response.statusCode).toBe(400);
      expect(response.json().code).toBe("VALIDATION_ERROR");
    });
  });

  // ---------------------------------------------------------------------------
  // Login Tests
  // ---------------------------------------------------------------------------

  describe("POST /auth/login", () => {
    beforeEach(async () => {
      // Create a test user
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "loginuser",
          password: "TestPassword123",
        },
      });
    });

    it("should login with valid credentials", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "loginuser",
          password: "TestPassword123",
        },
      });

      expect(response.statusCode).toBe(200);
      const data = response.json();
      expect(data.ok).toBe(true);
      expect(data.user.username).toBe("loginuser");

      // Should set cookies
      const cookies = response.cookies;
      expect(cookies.find((c: any) => c.name === "access_token")).toBeDefined();
      expect(cookies.find((c: any) => c.name === "refresh_token")).toBeDefined();
    });

    it("should reject invalid password", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "loginuser",
          password: "WrongPassword",
        },
      });

      expect(response.statusCode).toBe(401);
      expect(response.json().code).toBe("INVALID_CREDENTIALS");
    });

    it("should reject non-existent user", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "nonexistent",
          password: "TestPassword123",
        },
      });

      expect(response.statusCode).toBe(401);
      expect(response.json().code).toBe("INVALID_CREDENTIALS");
    });

    it("should support rememberMe option", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "loginuser",
          password: "TestPassword123",
          rememberMe: true,
        },
      });

      expect(response.statusCode).toBe(200);
      const data = response.json();
      expect(data.ok).toBe(true);
    });
  });

  // ---------------------------------------------------------------------------
  // Token Refresh Tests
  // ---------------------------------------------------------------------------

  describe("POST /auth/refresh", () => {
    it("should refresh access token with valid refresh token", async () => {
      // Login first to get tokens
      const loginResponse = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "loginuser",
          password: "TestPassword123",
        },
      });

      // Need to create user first
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "refreshuser",
          password: "TestPassword123",
        },
      });

      const login = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "refreshuser",
          password: "TestPassword123",
        },
      });

      const refreshToken = login.cookies.find((c: any) => c.name === "refresh_token");

      const response = await app.inject({
        method: "POST",
        url: "/auth/refresh",
        cookies: {
          refresh_token: refreshToken?.value ?? "",
        },
      });

      expect(response.statusCode).toBe(200);
      expect(response.json().ok).toBe(true);
    });

    it("should reject without refresh token", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/refresh",
      });

      expect(response.statusCode).toBe(401);
      expect(response.json().code).toBe("NO_REFRESH_TOKEN");
    });

    it("should reject invalid refresh token", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/refresh",
        cookies: {
          refresh_token: "invalid-token",
        },
      });

      expect(response.statusCode).toBe(401);
      expect(response.json().code).toBe("INVALID_REFRESH_TOKEN");
    });
  });

  // ---------------------------------------------------------------------------
  // Logout Tests
  // ---------------------------------------------------------------------------

  describe("POST /auth/logout", () => {
    it("should logout and clear cookies", async () => {
      // Register and login first
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "logoutuser",
          password: "TestPassword123",
        },
      });

      const login = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "logoutuser",
          password: "TestPassword123",
        },
      });

      const refreshToken = login.cookies.find((c: any) => c.name === "refresh_token");

      const response = await app.inject({
        method: "POST",
        url: "/auth/logout",
        cookies: {
          refresh_token: refreshToken?.value ?? "",
        },
      });

      expect(response.statusCode).toBe(200);
      expect(response.json().ok).toBe(true);
    });

    it("should succeed even without refresh token", async () => {
      const response = await app.inject({
        method: "POST",
        url: "/auth/logout",
      });

      expect(response.statusCode).toBe(200);
      expect(response.json().ok).toBe(true);
    });
  });

  // ---------------------------------------------------------------------------
  // Me Endpoint Tests
  // ---------------------------------------------------------------------------

  describe("GET /auth/me", () => {
    it("should return user info with valid access token", async () => {
      // Register and login
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "meuser",
          password: "TestPassword123",
        },
      });

      const login = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "meuser",
          password: "TestPassword123",
        },
      });

      const accessToken = login.cookies.find((c: any) => c.name === "access_token");

      const response = await app.inject({
        method: "GET",
        url: "/auth/me",
        cookies: {
          access_token: accessToken?.value ?? "",
        },
      });

      expect(response.statusCode).toBe(200);
      const data = response.json();
      expect(data.username).toBe("meuser");
    });

    it("should reject without access token", async () => {
      const response = await app.inject({
        method: "GET",
        url: "/auth/me",
      });

      expect(response.statusCode).toBe(401);
    });

    it("should reject with invalid access token", async () => {
      const response = await app.inject({
        method: "GET",
        url: "/auth/me",
        cookies: {
          access_token: "invalid.jwt.token",
        },
      });

      expect(response.statusCode).toBe(401);
    });
  });

  // ---------------------------------------------------------------------------
  // Inactive User Tests
  // ---------------------------------------------------------------------------

  describe("Inactive user handling", () => {
    it("should reject login for inactive user", async () => {
      // Create user
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "inactiveuser",
          password: "TestPassword123",
        },
      });

      // Manually deactivate user in DB
      await testClient.execute({
        sql: "UPDATE users SET is_active = 0 WHERE username = ?",
        args: ["inactiveuser"],
      });

      const response = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "inactiveuser",
          password: "TestPassword123",
        },
      });

      expect(response.statusCode).toBe(401);
      expect(response.json().code).toBe("ACCOUNT_DISABLED");
    });
  });

  // ---------------------------------------------------------------------------
  // Profile Update Tests
  // ---------------------------------------------------------------------------

  describe("PUT /auth/me (profile update)", () => {
    it("should update password with valid current password", async () => {
      // Register and login
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "profileuser",
          password: "TestPassword123",
        },
      });

      const login = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "profileuser",
          password: "TestPassword123",
        },
      });

      const accessToken = login.cookies.find((c: any) => c.name === "access_token");

      const response = await app.inject({
        method: "PUT",
        url: "/auth/me",
        cookies: {
          access_token: accessToken?.value ?? "",
        },
        payload: {
          currentPassword: "TestPassword123",
          newPassword: "NewPassword456",
        },
      });

      expect(response.statusCode).toBe(200);
      expect(response.json().ok).toBe(true);

      // Verify can login with new password
      const newLogin = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "profileuser",
          password: "NewPassword456",
        },
      });

      expect(newLogin.statusCode).toBe(200);
    });

    it("should reject password change without current password", async () => {
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "profileuser2",
          password: "TestPassword123",
        },
      });

      const login = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "profileuser2",
          password: "TestPassword123",
        },
      });

      const accessToken = login.cookies.find((c: any) => c.name === "access_token");

      const response = await app.inject({
        method: "PUT",
        url: "/auth/me",
        cookies: {
          access_token: accessToken?.value ?? "",
        },
        payload: {
          newPassword: "NewPassword456",
        },
      });

      expect(response.statusCode).toBe(400);
      expect(response.json().code).toBe("CURRENT_PASSWORD_REQUIRED");
    });

    it("should reject password change with wrong current password", async () => {
      await app.inject({
        method: "POST",
        url: "/auth/register",
        payload: {
          username: "profileuser3",
          password: "TestPassword123",
        },
      });

      const login = await app.inject({
        method: "POST",
        url: "/auth/login",
        payload: {
          username: "profileuser3",
          password: "TestPassword123",
        },
      });

      const accessToken = login.cookies.find((c: any) => c.name === "access_token");

      const response = await app.inject({
        method: "PUT",
        url: "/auth/me",
        cookies: {
          access_token: accessToken?.value ?? "",
        },
        payload: {
          currentPassword: "WrongPassword",
          newPassword: "NewPassword456",
        },
      });

      expect(response.statusCode).toBe(401);
      expect(response.json().code).toBe("INVALID_PASSWORD");
    });

    it("should reject profile update without auth", async () => {
      const response = await app.inject({
        method: "PUT",
        url: "/auth/me",
        payload: {
          currentPassword: "Test123",
          newPassword: "NewPassword456",
        },
      });

      expect(response.statusCode).toBe(401);
    });
  });
});
