import "./setup-env.js";
import { describe, expect, it, beforeAll, afterAll, vi } from "vitest";
import { eq, inArray, sql } from "drizzle-orm";
import { buildServer } from "../src/server.js";
import { loadConfig } from "../src/config.js";
import { createDatabase, closeDatabase, schema } from "@app/database";

/**
 * GDPR residual test (docs/29): after DELETE /v1/me, no personal trace of the
 * user must remain in the database, and every user-referencing column must be
 * known to this test. If a new user_id-like column is added, the schema-diff
 * assertion fails until the column is classified here.
 */
describe("DELETE /v1/me — GDPR residual completeness", () => {
  const testDb = createDatabase(process.env.TEST_DATABASE_URL!);
  const config = loadConfig();
  let app: Awaited<ReturnType<typeof buildServer>>;

  async function cleanupUser(email: string) {
    const existing = await testDb.db
      .select({ id: schema.users.id })
      .from(schema.users)
      .where(inArray(schema.users.email, [email, `deleted-${email}`]));
    for (const u of existing) {
      // Cascade-clean personal tables before hard-deleting the user.
      await testDb.db.delete(schema.aiCorrections).where(eq(schema.aiCorrections.userId, u.id));
      await testDb.db.delete(schema.scanJobs).where(eq(schema.scanJobs.userId, u.id));
      await testDb.db.delete(schema.recipeRatings).where(eq(schema.recipeRatings.userId, u.id));
      await testDb.db.delete(schema.recipeFavorites).where(eq(schema.recipeFavorites.userId, u.id));
      await testDb.db.delete(schema.recipeCooks).where(eq(schema.recipeCooks.userId, u.id));
      await testDb.db.delete(schema.creatorFollows).where(eq(schema.creatorFollows.followerUserId, u.id));
      await testDb.db.delete(schema.creatorFollows).where(eq(schema.creatorFollows.creatorUserId, u.id));
      await testDb.db.delete(schema.creatorStats).where(eq(schema.creatorStats.userId, u.id));
      await testDb.db.delete(schema.recipes).where(eq(schema.recipes.creatorUserId, u.id));
      await testDb.db.delete(schema.foodMemories).where(eq(schema.foodMemories.userId, u.id));
      await testDb.db.delete(schema.memoryItems).where(eq(schema.memoryItems.userId, u.id));
      await testDb.db.delete(schema.tasteSignals).where(eq(schema.tasteSignals.userId, u.id));
      await testDb.db.delete(schema.meals).where(eq(schema.meals.userId, u.id));
      await testDb.db.delete(schema.subscriptions).where(eq(schema.subscriptions.userId, u.id));
      await testDb.db.delete(schema.subscriptionEvents).where(eq(schema.subscriptionEvents.userId, u.id));
      await testDb.db.delete(schema.storeTransactions).where(eq(schema.storeTransactions.userId, u.id));
      await testDb.db.delete(schema.trials).where(eq(schema.trials.userId, u.id));
      await testDb.db.delete(schema.aiUsageCounters).where(eq(schema.aiUsageCounters.userId, u.id));
      await testDb.db.delete(schema.notifications).where(eq(schema.notifications.userId, u.id));
      await testDb.db.delete(schema.pushTokens).where(eq(schema.pushTokens.userId, u.id));
      await testDb.db.delete(schema.idempotencyKeys).where(eq(schema.idempotencyKeys.userId, u.id));
      await testDb.db.delete(schema.userConsents).where(eq(schema.userConsents.userId, u.id));
      await testDb.db.delete(schema.userPreferences).where(eq(schema.userPreferences.userId, u.id));
      await testDb.db.delete(schema.userHealthProfiles).where(eq(schema.userHealthProfiles.userId, u.id));
      await testDb.db.delete(schema.userLocalePreferences).where(eq(schema.userLocalePreferences.userId, u.id));
      await testDb.db.delete(schema.userCredentials).where(eq(schema.userCredentials.userId, u.id));
      await testDb.db.delete(schema.refreshTokens).where(eq(schema.refreshTokens.userId, u.id));
      await testDb.db.delete(schema.emailVerificationTokens).where(eq(schema.emailVerificationTokens.userId, u.id));
      await testDb.db.delete(schema.passwordResetTokens).where(eq(schema.passwordResetTokens.userId, u.id));
      await testDb.db.delete(schema.adminTotp).where(eq(schema.adminTotp.userId, u.id));
      await testDb.db.delete(schema.auditLogs).where(eq(schema.auditLogs.actorUserId, u.id));
      await testDb.db.delete(schema.domainEvents).where(eq(schema.domainEvents.userId, u.id));
      await testDb.db.delete(schema.productAnalyticsEvents).where(eq(schema.productAnalyticsEvents.userId, u.id));
      // Remove memberships and any orphaned single-member households.
      const memberships = await testDb.db
        .select({ householdId: schema.householdMembers.householdId })
        .from(schema.householdMembers)
        .where(eq(schema.householdMembers.userId, u.id));
      await testDb.db.delete(schema.householdMembers).where(eq(schema.householdMembers.userId, u.id));
      for (const { householdId } of memberships) {
        const remaining = await testDb.db
          .select({ count: sql<number>`count(*)::int` })
          .from(schema.householdMembers)
          .where(eq(schema.householdMembers.householdId, householdId));
        if (Number(remaining[0]?.count ?? 0) === 0) {
          await testDb.db.delete(schema.households).where(eq(schema.households.id, householdId));
        }
      }
      await testDb.db.delete(schema.users).where(eq(schema.users.id, u.id));
    }
  }

  async function registerUser(email: string) {
    const res = await app.inject({
      method: "POST",
      url: "/v1/auth/register",
      payload: { email, password: "Password123!", displayName: "GDPR Residual Test" },
    });
    const body = JSON.parse(res.body) as { accessToken: string };
    const token = body.accessToken;
    const userId = (JSON.parse(atob(token.split(".")[1]!)) as { sub: string }).sub;

    await app.inject({
      method: "POST",
      url: "/v1/onboarding/quick-start",
      headers: { authorization: `Bearer ${token}` },
      payload: { goals: ["less_waste"], precisionMode: "simple" },
    });

    return { token, userId };
  }

  beforeAll(async () => {
    app = await buildServer(config);
    await app.ready();
  });

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

  async function getHouseholdId(userId: string) {
    const [row] = await testDb.db
      .select({ householdId: schema.householdMembers.householdId })
      .from(schema.householdMembers)
      .where(eq(schema.householdMembers.userId, userId))
      .limit(1);
    return row?.householdId ?? null;
  }

  it("lämnar inga personliga spår efter DELETE /v1/me", async () => {
    const email = "gdpr-residual@example.invalid";
    await cleanupUser(email);
    const { token, userId } = await registerUser(email);

    // Seed a representative slice of every personal data category.
    await testDb.db.insert(schema.userConsents).values([
      { userId, kind: "personalization", status: "granted" },
      { userId, kind: "image_training", status: "granted" },
    ]);
    await testDb.db.insert(schema.pushTokens).values({ userId, token: "expo-token-1", platform: "ios" });
    await testDb.db.insert(schema.notifications).values({
      userId,
      type: "subscription_status",
      titleSv: "Hej",
      bodySv: "Välkommen",
    });
    await testDb.db.insert(schema.idempotencyKeys).values({
      userId,
      key: "key-1",
      endpoint: "POST /v1/me",
      responseStatus: 200,
    });

    const [scanJob] = await testDb.db
      .insert(schema.scanJobs)
      .values({
        userId,
        scanType: "fridge",
        jobType: "ANALYZE_FRIDGE_IMAGE",
        status: "awaiting_confirmation",
        s3Keys: ["fridge-scans/residual-a.jpg"],
      })
      .returning();

    const [correction] = await testDb.db
      .insert(schema.aiCorrections)
      .values({
        scanJobId: scanJob!.id,
        userId,
        taskType: "ANALYZE_FRIDGE_IMAGE",
        aiOutput: {},
        userCorrection: {},
        imageS3Key: "fridge-scans/residual-correction.jpg",
        consentSnapshot: {},
      })
      .returning();

    await testDb.db.insert(schema.aiTrainingBank).values({
      correctionId: correction!.id,
      taskType: "ANALYZE_FRIDGE_IMAGE",
      imageS3Key: "fridge-scans/residual-training.jpg",
      proposal: {},
      action: "accept",
      consentSnapshot: {},
    });

    await testDb.db.insert(schema.foodMemories).values({
      userId,
      titleSv: "Mormors köttbullar",
      summarySv: "Gott",
      occurredAt: new Date(),
      photoUrl: await app.storage.getReadUrl("food-memories/residual-memory.jpg"),
    });

    await testDb.db.insert(schema.memoryItems).values({
      userId,
      kind: "structured_fact",
      key: "likes-pasta",
      summarySv: "Gillar pasta",
      origin: "user_stated",
    });

    await testDb.db.insert(schema.tasteSignals).values({
      userId,
      axis: "sweetness",
      direction: 0.5,
      origin: "user_stated",
    });

    await testDb.db.insert(schema.meals).values({
      userId,
      householdId: (await getHouseholdId(userId))!,
      mealType: "lunch",
      source: "manual",
      titleSv: "Resttest-lunch",
      date: new Date().toISOString().slice(0, 10),
      nutrition: { kcal: 0, proteinG: 0, carbsG: 0, fatG: 0, saturatedFatG: 0, fiberG: 0, sugarG: 0, saltG: 0 },
    });

    const [draftRecipe] = await testDb.db
      .insert(schema.recipes)
      .values({
        slug: `gdpr-draft-${userId.slice(0, 8)}`,
        titleSv: "GDPR Draft",
        descriptionSv: "",
        cuisine: "swedish",
        sourceType: "user_generated",
        creatorUserId: userId,
        creatorDisplayName: "Test",
        status: "draft",
        nutritionPerPortion: { kcal: 0, proteinG: 0, carbsG: 0, fatG: 0, saturatedFatG: 0, fiberG: 0, sugarG: 0, saltG: 0 },
        dna: {
          cuisine: "swedish",
          vegetables: [],
          flavorProfile: [],
          spiceLevel: 0,
          method: "stovetop",
          timeMinutes: 0,
          calories: 0,
          proteinGrams: 0,
        },
      })
      .returning();

    const [publishedRecipe] = await testDb.db
      .insert(schema.recipes)
      .values({
        slug: `gdpr-published-${userId.slice(0, 8)}`,
        titleSv: "GDPR Public",
        descriptionSv: "",
        cuisine: "swedish",
        sourceType: "user_generated",
        creatorUserId: userId,
        creatorDisplayName: "Test",
        status: "published",
        nutritionPerPortion: { kcal: 0, proteinG: 0, carbsG: 0, fatG: 0, saturatedFatG: 0, fiberG: 0, sugarG: 0, saltG: 0 },
        dna: {
          cuisine: "swedish",
          vegetables: [],
          flavorProfile: [],
          spiceLevel: 0,
          method: "stovetop",
          timeMinutes: 0,
          calories: 0,
          proteinGrams: 0,
        },
      })
      .returning();

    await testDb.db.insert(schema.recipeRatings).values({ recipeId: publishedRecipe!.id, userId, stars: 5 });
    await testDb.db.insert(schema.recipeFavorites).values({ recipeId: publishedRecipe!.id, userId });
    await testDb.db.insert(schema.recipeCooks).values({ recipeId: publishedRecipe!.id, userId, portionsCooked: 2 });
    await testDb.db.insert(schema.creatorStats).values({ userId });

    await testDb.db.insert(schema.subscriptions).values({
      userId,
      provider: "apple",
      productId: "premium.monthly",
      plan: "household",
      status: "active",
    });

    await testDb.db.insert(schema.storeTransactions).values({
      userId,
      provider: "apple",
      transactionId: `tx-${userId.slice(0, 8)}`,
      rawPayload: { receipt: "secret" },
    });

    await testDb.db.insert(schema.aiUsageCounters).values({
      userId,
      month: "2026-08",
      aiScans: 1,
    });

    await testDb.db.insert(schema.auditLogs).values({
      actorUserId: userId,
      action: "test.action",
      ip: "1.2.3.4",
      metadata: { note: "before deletion" },
    });

    await testDb.db.insert(schema.domainEvents).values({
      type: "RECIPE_CREATED",
      userId,
      payload: { email },
    });

    await testDb.db.insert(schema.productAnalyticsEvents).values({
      userId,
      eventName: "test_event",
      properties: {},
    });

    vi.spyOn(app.storage, "deleteObject").mockResolvedValue(undefined);

    const res = await app.inject({
      method: "DELETE",
      url: "/v1/me",
      headers: { authorization: `Bearer ${token}` },
    });
    expect(res.statusCode).toBe(200);

    // --- Schema-diff assertion: every user-referencing column must be classified ---
    const userRefColumns = await testDb.db.execute<{
      table_name: string;
      column_name: string;
    }>(sql`
      SELECT table_name, column_name
      FROM information_schema.columns
      WHERE table_schema = 'public'
        AND column_name IN (
          'user_id', 'actor_user_id', 'creator_user_id', 'started_by_user_id',
          'follower_user_id', 'reserved_for_user_id', 'added_by_user_id', 'resolved_by_user_id'
        )
        AND table_name NOT LIKE 'pg_%'
        AND table_name NOT IN ('__drizzle_migrations')
      ORDER BY table_name, column_name
    `);

    const classified: Record<string, string[]> = {
      // Hard-deleted during erasure.
      users: [], // user_id is the primary key; checked separately.
      user_credentials: ["user_id"],
      admin_totp: ["user_id"],
      email_verification_tokens: ["user_id"],
      password_reset_tokens: ["user_id"],
      refresh_tokens: ["user_id"],
      user_consents: ["user_id"],
      user_preferences: ["user_id"],
      user_health_profiles: ["user_id"],
      user_locale_preferences: ["user_id"],
      idempotency_keys: ["user_id"],
      push_tokens: ["user_id"],
      notifications: ["user_id"],
      taste_signals: ["user_id"],
      memory_items: ["user_id"],
      food_memories: ["user_id"],
      meals: ["user_id"],
      scan_jobs: ["user_id"],
      ai_corrections: ["user_id"],
      ai_training_bank: [], // joined via correction_id, user_id not present.
      ai_usage_counters: ["user_id"],
      trials: ["user_id"],
      recipe_ratings: ["user_id"],
      recipe_favorites: ["user_id"],
      recipe_cooks: ["user_id"],
      creator_stats: ["user_id"],
      creator_follows: ["follower_user_id", "creator_user_id"],
      product_analytics_events: ["user_id"],
      // Anonymized / pseudonymized.
      subscriptions: ["user_id", "pseudonym"],
      subscription_events: ["user_id", "pseudonym"],
      store_transactions: ["user_id", "pseudonym"],
      gdpr_retention_pseudonyms: ["user_id"],
      audit_logs: ["actor_user_id"],
      domain_events: ["user_id"],
      recipes: ["creator_user_id"],
      cooking_sessions: ["started_by_user_id"],
      // Household-level; user removed via household_members.
      household_members: ["user_id"],
      // Anonymized in surviving households.
      inventory_conflicts: ["resolved_by_user_id"],
      inventory_transactions: ["actor_user_id"],
      meal_boxes: ["reserved_for_user_id"],
      shopping_list_items: ["added_by_user_id"],
    };

    for (const { table_name, column_name } of userRefColumns.rows) {
      const allowed = classified[table_name];
      expect(
        allowed?.includes(column_name) ?? false,
        `Column ${table_name}.${column_name} is not classified in the GDPR residual test. Add it to the classified map with the correct erasure action.`,
      ).toBe(true);
    }

    // --- Residual checks: no row may still reference the deleted user ---
    const checks: Array<[string, string]> = [
      ["user_credentials", "user_id"],
      ["admin_totp", "user_id"],
      ["email_verification_tokens", "user_id"],
      ["password_reset_tokens", "user_id"],
      ["refresh_tokens", "user_id"],
      ["user_consents", "user_id"],
      ["user_preferences", "user_id"],
      ["user_health_profiles", "user_id"],
      ["user_locale_preferences", "user_id"],
      ["idempotency_keys", "user_id"],
      ["push_tokens", "user_id"],
      ["notifications", "user_id"],
      ["taste_signals", "user_id"],
      ["memory_items", "user_id"],
      ["food_memories", "user_id"],
      ["meals", "user_id"],
      ["scan_jobs", "user_id"],
      ["ai_corrections", "user_id"],
      ["ai_usage_counters", "user_id"],
      ["trials", "user_id"],
      ["recipe_ratings", "user_id"],
      ["recipe_favorites", "user_id"],
      ["recipe_cooks", "user_id"],
      ["creator_stats", "user_id"],
      ["creator_follows", "follower_user_id"],
      ["creator_follows", "creator_user_id"],
      ["product_analytics_events", "user_id"],
      ["subscriptions", "user_id"],
      ["subscription_events", "user_id"],
      ["store_transactions", "user_id"],
      ["audit_logs", "actor_user_id"],
      ["domain_events", "user_id"],
      ["recipes", "creator_user_id"],
      ["cooking_sessions", "started_by_user_id"],
      ["household_members", "user_id"],
      ["inventory_conflicts", "resolved_by_user_id"],
      ["inventory_transactions", "actor_user_id"],
      ["meal_boxes", "reserved_for_user_id"],
      ["shopping_list_items", "added_by_user_id"],
    ];

    for (const [table, column] of checks) {
      const result = await testDb.db.execute<{ count: number }>(
        sql.raw(`SELECT count(*)::int AS count FROM "${table}" WHERE "${column}" = '${userId}'`),
      );
      expect(Number(result.rows[0]?.count ?? 0), `Residual ${table}.${column} for deleted user`).toBe(0);
    }

    // Receipts in surviving households must have image stripped.
    const survivingReceipts = await testDb.db
      .select({ id: schema.receipts.id, imageUrl: schema.receipts.imageUrl })
      .from(schema.receipts)
      .where(eq(schema.receipts.householdId, (await getHouseholdId(userId))!));
    for (const r of survivingReceipts) {
      expect(r.imageUrl, "Receipt image must be null after deletion in surviving household").toBeNull();
    }

    // Public recipe must be anonymized.
    const [publicAfter] = await testDb.db
      .select({ creatorUserId: schema.recipes.creatorUserId, creatorDisplayName: schema.recipes.creatorDisplayName })
      .from(schema.recipes)
      .where(eq(schema.recipes.id, publishedRecipe!.id));
    expect(publicAfter?.creatorUserId).toBeNull();
    expect(publicAfter?.creatorDisplayName).toBeNull();

    // Draft recipe must be deleted.
    const draftAfter = await testDb.db
      .select({ id: schema.recipes.id })
      .from(schema.recipes)
      .where(eq(schema.recipes.id, draftRecipe!.id));
    expect(draftAfter).toHaveLength(0);

    // User row is soft-deleted and anonymized.
    const [userAfter] = await testDb.db
      .select({ email: schema.users.email, displayName: schema.users.displayName, deletedAt: schema.users.deletedAt })
      .from(schema.users)
      .where(eq(schema.users.id, userId));
    expect(userAfter?.deletedAt).not.toBeNull();
    expect(userAfter?.email).toContain("anonymized.invalid");
    expect(userAfter?.displayName).toBe("Raderad användare");

    await cleanupUser(email);
  });
});
