import type { SignupInput } from "@batch-cooking/shared";
import { faker } from "@faker-js/faker";
import { expect } from "chai";
import request from "supertest";
import { createApp } from "../../src/app.js";
import { prisma } from "../../src/db/prisma.js";
import { findImportedRecipeIds, syncRecipeSources } from "../../src/db/recipe-source-sync.js";
import type { RecipeSourceAdapter } from "../../src/lib/recipe-sources/recipe-source-adapter.js";
import {
  clearRecipeSources,
  registerRecipeSource,
} from "../../src/lib/recipe-sources/recipe-source-registry.js";
import { resetDatabase } from "../../test-support/reset-db.js";

/** See `recipe.test.ts` — generated rather than hardcoded, no test fixture looks like a real person's data. */
function buildSignupPayload(): SignupInput {
  const firstName = faker.person.firstName();
  const lastName = faker.person.lastName();
  return {
    firstName,
    lastName,
    email: faker.internet.email({ firstName, lastName }).toLowerCase(),
    password: faker.internet.password({ length: 16 }),
  };
}

/** A minimal `RecipeSourceAdapter` whose list/fetchDetail/parse are never actually called here — only `key`/`name`/`official`/`iconUrl` matter for exercising `syncRecipeSources`. */
function buildFakeAdapter(key: string, name: string): RecipeSourceAdapter {
  return {
    key,
    name,
    official: false,
    iconUrl: null,
    locale: "fr",
    async list() {
      return { items: [], nextCursor: null };
    },
    async fetchDetail() {
      throw new Error("not implemented");
    },
    parse() {
      throw new Error("not implemented");
    },
  };
}

describe("recipe-source-sync", () => {
  const app = createApp();

  async function signup(): Promise<{ profileId: number }> {
    const res = await request.agent(app).post("/auth/signup").send(buildSignupPayload());
    return { profileId: res.body.id };
  }

  beforeEach(async () => {
    await resetDatabase();
    clearRecipeSources();
  });

  afterEach(() => {
    clearRecipeSources();
  });

  after(async () => {
    await prisma.$disconnect();
  });

  describe("syncRecipeSources", () => {
    it("does nothing when the registry is empty", async () => {
      await syncRecipeSources(prisma);
      expect(await prisma.source.count()).to.equal(0);
    });

    it("creates a Source row per registered adapter", async () => {
      registerRecipeSource(buildFakeAdapter("fakeSource", "Fake Source"));

      await syncRecipeSources(prisma);

      const source = await prisma.source.findUniqueOrThrow({ where: { key: "fakeSource" } });
      expect(source.name).to.equal("Fake Source");
    });

    it("is idempotent — running it twice doesn't duplicate rows", async () => {
      registerRecipeSource(buildFakeAdapter("fakeSource", "Fake Source"));

      await syncRecipeSources(prisma);
      await syncRecipeSources(prisma);

      expect(await prisma.source.count()).to.equal(1);
    });

    it("updates the name when the adapter's own name changes between syncs", async () => {
      registerRecipeSource(buildFakeAdapter("fakeSource", "Old Name"));
      await syncRecipeSources(prisma);
      clearRecipeSources();
      registerRecipeSource(buildFakeAdapter("fakeSource", "New Name"));

      await syncRecipeSources(prisma);

      const source = await prisma.source.findUniqueOrThrow({ where: { key: "fakeSource" } });
      expect(source.name).to.equal("New Name");
    });

    it("never deletes a Source row whose key fell out of the registry", async () => {
      registerRecipeSource(buildFakeAdapter("fakeSource", "Fake Source"));
      await syncRecipeSources(prisma);
      clearRecipeSources();

      await syncRecipeSources(prisma);

      expect(await prisma.source.count()).to.equal(1);
    });
  });

  describe("findImportedRecipeIds", () => {
    it("returns an empty map for a sourceKey with no matching Source row", async () => {
      expect(await findImportedRecipeIds(prisma, "unknown", ["1", "2"])).to.deep.equal(new Map());
    });

    it("returns an empty map for an empty externalIds list", async () => {
      expect(await findImportedRecipeIds(prisma, "fakeSource", [])).to.deep.equal(new Map());
    });

    it("returns exactly the externalIds already imported from that source, mapped to their Recipe id", async () => {
      const { profileId } = await signup();
      registerRecipeSource(buildFakeAdapter("fakeSource", "Fake Source"));
      await syncRecipeSources(prisma);
      const source = await prisma.source.findUniqueOrThrow({ where: { key: "fakeSource" } });

      const imported = await prisma.recipe.create({
        data: {
          name: "Tarte",
          authorId: profileId,
          portions: 4,
          sourceId: source.id,
          externalId: "1",
        },
      });
      // Manually-authored, not tied to any source — shouldn't ever show up as "imported".
      await prisma.recipe.create({ data: { name: "Salade", authorId: profileId, portions: 2 } });

      const result = await findImportedRecipeIds(prisma, "fakeSource", ["1", "2", "3"]);
      expect(result).to.deep.equal(new Map([["1", imported.id]]));
    });

    it("scopes matches to the given source — the same externalId from a different source doesn't count", async () => {
      const { profileId } = await signup();
      registerRecipeSource(buildFakeAdapter("fakeSource", "Fake Source"));
      registerRecipeSource(buildFakeAdapter("otherSource", "Other Source"));
      await syncRecipeSources(prisma);
      const otherSource = await prisma.source.findUniqueOrThrow({ where: { key: "otherSource" } });

      await prisma.recipe.create({
        data: {
          name: "Tarte",
          authorId: profileId,
          portions: 4,
          sourceId: otherSource.id,
          externalId: "1",
        },
      });

      expect(await findImportedRecipeIds(prisma, "fakeSource", ["1"])).to.deep.equal(new Map());
    });
  });

  describe("Recipe(sourceId, externalId) uniqueness", () => {
    it("rejects importing the same source recipe twice", async () => {
      const { profileId } = await signup();
      registerRecipeSource(buildFakeAdapter("fakeSource", "Fake Source"));
      await syncRecipeSources(prisma);
      const source = await prisma.source.findUniqueOrThrow({ where: { key: "fakeSource" } });

      await prisma.recipe.create({
        data: {
          name: "Tarte",
          authorId: profileId,
          portions: 4,
          sourceId: source.id,
          externalId: "1",
        },
      });

      let rejected = false;
      try {
        await prisma.recipe.create({
          data: {
            name: "Tarte (again)",
            authorId: profileId,
            portions: 4,
            sourceId: source.id,
            externalId: "1",
          },
        });
      } catch {
        rejected = true;
      }
      expect(rejected).to.be.true;
    });

    it("allows any number of manually-authored recipes (both columns null)", async () => {
      const { profileId } = await signup();

      await prisma.recipe.create({ data: { name: "Une", authorId: profileId, portions: 4 } });
      await prisma.recipe.create({ data: { name: "Deux", authorId: profileId, portions: 4 } });

      expect(await prisma.recipe.count()).to.equal(2);
    });
  });
});
