import type {
  Entity,
  EntityKind,
  GraphProvider,
  Property,
  Relation,
} from "@enesis/editor"
import { describe, expect, it } from "vitest"
import { GraphService } from "./graph"

function mockEntity(
  id: string,
  kind: EntityKind,
  overrides?: Partial<Entity>,
): Entity {
  return { id, kind, title: id, ...overrides }
}

/**
 * In-memory GraphProvider for testing. Stores entities, relations, and
 * properties in plain arrays so tests don't need a SQLite database.
 */
class MemoryGraphProvider implements GraphProvider {
  entities: Entity[] = []
  relations: Relation[] = []
  properties: Property[] = []

  async getEntities(kind?: EntityKind): Promise<Entity[]> {
    if (kind) return this.entities.filter((e) => e.kind === kind)
    return [...this.entities]
  }

  async getEntity(id: string): Promise<Entity | undefined> {
    return this.entities.find((e) => e.id === id)
  }

  async getEntitiesByIds(ids: string[]): Promise<Entity[]> {
    const idSet = new Set(ids)
    return this.entities.filter((e) => idSet.has(e.id))
  }

  async getRelations(
    from?: string,
    to?: string,
    type?: Relation["type"],
  ): Promise<Relation[]> {
    return this.relations.filter((r) => {
      if (from && r.from !== from) return false
      if (to && r.to !== to) return false
      if (type && r.type !== type) return false
      return true
    })
  }

  async getProperties(entityId?: string, key?: string): Promise<Property[]> {
    return this.properties.filter((p) => {
      if (entityId && p.entityId !== entityId) return false
      if (key && p.key !== key) return false
      return true
    })
  }
}

function createService(): { svc: GraphService; provider: MemoryGraphProvider } {
  const provider = new MemoryGraphProvider()
  const svc = new GraphService(provider)
  return { svc, provider }
}

describe("GraphService", () => {
  describe("getEntity", () => {
    it("returns the entity when it exists", async () => {
      const { svc, provider } = createService()
      provider.entities.push(mockEntity("pages/Foo.md", "page"))

      const result = await svc.getEntity("pages/Foo.md")
      expect(result).toBeDefined()
      expect(result?.id).toBe("pages/Foo.md")
      expect(result?.kind).toBe("page")
    })

    it("returns undefined when the entity does not exist", async () => {
      const { svc } = createService()
      expect(await svc.getEntity("nonexistent")).toBeUndefined()
    })
  })

  describe("getEntitiesByIds", () => {
    it("returns matching entities", async () => {
      const { svc, provider } = createService()
      provider.entities.push(
        mockEntity("a", "page"),
        mockEntity("b", "page"),
        mockEntity("c", "page"),
      )

      const result = await svc.getEntitiesByIds(["a", "c"])
      expect(result).toHaveLength(2)
      expect(result.map((e) => e.id).sort()).toEqual(["a", "c"])
    })

    it("silently drops missing IDs", async () => {
      const { svc, provider } = createService()
      provider.entities.push(mockEntity("a", "page"))

      const result = await svc.getEntitiesByIds(["a", "missing"])
      expect(result).toHaveLength(1)
      expect(result[0].id).toBe("a")
    })

    it("returns empty array for empty input", async () => {
      const { svc } = createService()
      expect(await svc.getEntitiesByIds([])).toEqual([])
    })
  })

  describe("getIncoming", () => {
    it("returns entities that reference the target", async () => {
      const { svc, provider } = createService()
      provider.entities.push(
        mockEntity("block-1", "block", { path: "pages/A.md" }),
        mockEntity("block-2", "block", { path: "pages/B.md" }),
      )
      provider.relations.push(
        { from: "block-1", to: "Target Page", type: "references" },
        { from: "block-2", to: "Target Page", type: "references" },
      )

      const incoming = await svc.getIncoming("Target Page")
      expect(incoming).toHaveLength(2)
      expect(incoming.map((i) => i.from.id).sort()).toEqual([
        "block-1",
        "block-2",
      ])
    })

    it("returns empty when no relations exist", async () => {
      const { svc } = createService()
      expect(await svc.getIncoming("nobody-links-to-me")).toEqual([])
    })

    it("filters out relations whose source entity was deleted", async () => {
      const { svc, provider } = createService()
      provider.entities.push(mockEntity("block-1", "block"))
      provider.relations.push(
        { from: "block-1", to: "Target", type: "references" },
        { from: "deleted-block", to: "Target", type: "references" },
      )

      const incoming = await svc.getIncoming("Target")
      expect(incoming).toHaveLength(1)
      expect(incoming[0].from.id).toBe("block-1")
    })
  })

  describe("getOutgoing", () => {
    it("returns entities that the source references", async () => {
      const { svc, provider } = createService()
      provider.entities.push(
        mockEntity("page-1", "page", { id: "pages/A.md" }),
        mockEntity("block-1", "block"),
      )
      provider.relations.push({
        from: "block-1",
        to: "pages/A.md",
        type: "references",
      })

      const outgoing = await svc.getOutgoing("block-1")
      expect(outgoing).toHaveLength(1)
      expect(outgoing[0].to.id).toBe("pages/A.md")
    })

    it("returns empty when the source has no relations", async () => {
      const { svc, provider } = createService()
      provider.entities.push(mockEntity("block-1", "block"))

      expect(await svc.getOutgoing("block-1")).toEqual([])
    })
  })

  describe("getBacklinks", () => {
    it("returns same result as getIncoming", async () => {
      const { svc, provider } = createService()
      provider.entities.push(mockEntity("block-1", "block"))
      provider.relations.push({
        from: "block-1",
        to: "My Page",
        type: "references",
      })

      const backlinks = await svc.getBacklinks("My Page")
      const incoming = await svc.getIncoming("My Page")
      expect(backlinks).toEqual(incoming)
    })
  })

  describe("getProperties", () => {
    it("returns properties for a given entity", async () => {
      const { svc, provider } = createService()
      provider.properties.push(
        { entityId: "block-1", key: "status", value: "active" },
        { entityId: "block-1", key: "priority", value: "high" },
      )

      const props = await svc.getProperties("block-1")
      expect(props).toHaveLength(2)
      expect(props.find((p) => p.key === "status")?.value).toBe("active")
    })

    it("returns empty array when entity has no properties", async () => {
      const { svc } = createService()
      expect(await svc.getProperties("block-without-props")).toEqual([])
    })
  })

  describe("queryByProperty", () => {
    it("finds entities by key and value", async () => {
      const { svc, provider } = createService()
      provider.entities.push(
        mockEntity("block-1", "block", { path: "pages/A.md" }),
        mockEntity("block-2", "block", { path: "pages/B.md" }),
        mockEntity("block-3", "block", { path: "pages/C.md" }),
      )
      provider.properties.push(
        { entityId: "block-1", key: "priority", value: "high" },
        { entityId: "block-2", key: "priority", value: "low" },
        { entityId: "block-3", key: "status", value: "active" },
      )

      const high = await svc.queryByProperty("priority", "high")
      expect(high).toHaveLength(1)
      expect(high[0].id).toBe("block-1")
    })

    it("finds all entities with a given key when value is omitted", async () => {
      const { svc, provider } = createService()
      provider.entities.push(
        mockEntity("block-1", "block"),
        mockEntity("block-2", "block"),
      )
      provider.properties.push(
        { entityId: "block-1", key: "status", value: "active" },
        { entityId: "block-2", key: "status", value: "archived" },
      )

      const all = await svc.queryByProperty("status")
      expect(all).toHaveLength(2)
    })

    it("returns empty when no properties match", async () => {
      const { svc } = createService()
      expect(await svc.queryByProperty("nonexistent")).toEqual([])
    })

    it("skips properties whose entity was deleted", async () => {
      const { svc, provider } = createService()
      provider.properties.push(
        { entityId: "existing-block", key: "status", value: "active" },
        { entityId: "deleted-block", key: "status", value: "archived" },
      )
      provider.entities.push(mockEntity("existing-block", "block"))

      const result = await svc.queryByProperty("status")
      expect(result).toHaveLength(1)
      expect(result[0].id).toBe("existing-block")
    })
  })

  describe("findRelations", () => {
    it("returns all relations with no filter", async () => {
      const { svc, provider } = createService()
      provider.relations.push(
        { from: "a", to: "b", type: "references" },
        { from: "c", to: "d", type: "tagged" },
      )

      const all = await svc.findRelations()
      expect(all).toHaveLength(2)
    })

    it("filters by type", async () => {
      const { svc, provider } = createService()
      provider.relations.push(
        { from: "a", to: "b", type: "references" },
        { from: "c", to: "d", type: "tagged" },
      )

      const tagged = await svc.findRelations({ type: "tagged" })
      expect(tagged).toHaveLength(1)
      expect(tagged[0].type).toBe("tagged")
    })
  })
})
