import { describe, it, expect } from "vitest"; import { getPlantById, getDiseaseById, getDiseasesByPlantId, getPlantWithDiseases, getDiseaseWithPlant, getLookalikeDiseases, searchPlants, searchDiseases, listPlants, listDiseases, validateKnowledgeBase, plants, diseases, } from "@/lib/api/diseases"; describe("Knowledge Base Data", () => { it("has ≥20 plants", () => { expect(plants.length).toBeGreaterThanOrEqual(20); }); it("has ≥80 diseases", () => { expect(diseases.length).toBeGreaterThanOrEqual(80); }); it("passes cross-reference validation (no errors)", () => { const errors = validateKnowledgeBase(); expect(errors).toEqual([]); }); }); describe("getPlantById", () => { it("returns plant for known ID", () => { const plant = getPlantById("tomato"); expect(plant).toBeDefined(); expect(plant!.commonName).toBe("Tomato"); expect(plant!.scientificName).toBe("Solanum lycopersicum"); }); it("returns undefined for unknown ID", () => { expect(getPlantById("nonexistent")).toBeUndefined(); }); it("is case-insensitive", () => { const plant = getPlantById("TOMATO"); expect(plant).toBeDefined(); expect(plant!.commonName).toBe("Tomato"); }); }); describe("getDiseaseById", () => { it("returns disease for known ID", () => { const disease = getDiseaseById("early-blight"); expect(disease).toBeDefined(); expect(disease!.name).toBe("Early Blight"); expect(disease!.plantId).toBe("tomato"); }); it("returns undefined for unknown ID", () => { expect(getDiseaseById("nonexistent")).toBeUndefined(); }); }); describe("getDiseasesByPlantId", () => { it("returns diseases for tomato", () => { const diseases = getDiseasesByPlantId("tomato"); expect(diseases.length).toBeGreaterThanOrEqual(3); expect(diseases.every((d) => d.plantId === "tomato")).toBe(true); }); it("returns empty array for plant with no diseases", () => { const diseases = getDiseasesByPlantId("nonexistent"); expect(diseases).toEqual([]); }); }); describe("getPlantWithDiseases", () => { it("returns plant with diseases for known ID", () => { const result = getPlantWithDiseases("tomato"); expect(result).toBeDefined(); expect(result!.plant.id).toBe("tomato"); expect(result!.diseases.length).toBeGreaterThanOrEqual(3); }); it("returns undefined for unknown ID", () => { expect(getPlantWithDiseases("nonexistent")).toBeUndefined(); }); }); describe("getDiseaseWithPlant", () => { it("returns disease with plant for known ID", () => { const result = getDiseaseWithPlant("early-blight"); expect(result).toBeDefined(); expect(result!.disease.id).toBe("early-blight"); expect(result!.plant.id).toBe("tomato"); }); it("returns undefined for unknown ID", () => { expect(getDiseaseWithPlant("nonexistent")).toBeUndefined(); }); }); describe("getLookalikeDiseases", () => { it("returns lookalike diseases for early blight", () => { const lookalikes = getLookalikeDiseases("early-blight"); expect(lookalikes.length).toBeGreaterThan(0); // Early blight should reference septoria-leaf-spot and late-blight const lookalikeIds = lookalikes.map((d) => d.id); expect(lookalikeIds).toContain("septoria-leaf-spot"); expect(lookalikeIds).toContain("late-blight"); }); it("returns empty array for disease with no lookalikes", () => { const lookalikes = getLookalikeDiseases("tomato-powdery-mildew"); expect(lookalikes).toEqual([]); }); }); describe("searchPlants", () => { it("returns all plants for empty search", () => { const results = searchPlants(""); expect(results).toEqual(plants); }); it("finds tomato by common name", () => { const results = searchPlants("tomato"); expect(results.length).toBeGreaterThan(0); expect(results.some((p) => p.id === "tomato")).toBe(true); }); it("finds plants by scientific name", () => { const results = searchPlants("Solanum"); expect(results.length).toBeGreaterThan(0); expect(results.every((p) => p.scientificName.includes("Solanum"))).toBe(true); }); it("finds plants by family", () => { const results = searchPlants("Lamiaceae"); expect(results.length).toBeGreaterThan(0); expect(results.every((p) => p.family === "Lamiaceae")).toBe(true); }); it("finds plants by category", () => { const results = searchPlants("houseplant"); expect(results.length).toBeGreaterThan(0); expect(results.every((p) => p.category === "houseplant")).toBe(true); }); it("returns empty array for no matches", () => { const results = searchPlants("xyznonexistent123"); expect(results).toEqual([]); }); }); describe("searchDiseases", () => { it("returns all diseases for empty search", () => { const results = searchDiseases(""); expect(results).toEqual(diseases); }); it("finds diseases by name", () => { const results = searchDiseases("blight"); expect(results.length).toBeGreaterThanOrEqual(2); }); it("finds diseases by scientific name", () => { const results = searchDiseases("Alternaria"); expect(results.length).toBeGreaterThan(0); }); it("finds diseases by description content", () => { const results = searchDiseases("calcium"); expect(results.length).toBeGreaterThan(0); }); it("finds diseases by symptom text", () => { const results = searchDiseases("powdery"); expect(results.length).toBeGreaterThan(0); }); it("returns empty array for no matches", () => { const results = searchDiseases("xyznonexistent123"); expect(results).toEqual([]); }); }); describe("listPlants", () => { it("returns all plants with no filters", () => { const results = listPlants(); expect(results).toEqual(plants); }); it("filters by category", () => { const results = listPlants({ category: "vegetable" }); expect(results.length).toBeGreaterThan(0); expect(results.every((p) => p.category === "vegetable")).toBe(true); }); it("combines search and category filter", () => { const results = listPlants({ search: "leaf", category: "houseplant" }); expect(results.every((p) => p.category === "houseplant")).toBe(true); }); }); describe("listDiseases", () => { it("returns all diseases with no filters", () => { const results = listDiseases(); expect(results).toEqual(diseases); }); it("filters by plantId", () => { const results = listDiseases({ plantId: "tomato" }); expect(results.length).toBeGreaterThan(0); expect(results.every((d) => d.plantId === "tomato")).toBe(true); }); it("filters by causalAgentType", () => { const results = listDiseases({ causalAgentType: "fungal" }); expect(results.length).toBeGreaterThan(0); expect(results.every((d) => d.causalAgentType === "fungal")).toBe(true); }); it("filters by severity", () => { const results = listDiseases({ severity: "critical" }); expect(results.length).toBeGreaterThan(0); expect(results.every((d) => d.severity === "critical")).toBe(true); }); it("combines plantId and search filters", () => { const results = listDiseases({ plantId: "tomato", search: "blight" }); expect(results.every((d) => d.plantId === "tomato")).toBe(true); expect(results.every((d) => d.name.toLowerCase().includes("blight") || d.description.toLowerCase().includes("blight") || d.symptoms.some((s) => s.toLowerCase().includes("blight")))).toBe(true); }); }); describe("validateKnowledgeBase", () => { it("returns no errors for valid data", () => { const errors = validateKnowledgeBase(); expect(errors).toEqual([]); }); it("detects invalid plant references", () => { // Temporarily modify a disease to have invalid plantId const original = diseases[0].plantId; diseases[0].plantId = "nonexistent-plant"; const errors = validateKnowledgeBase(); diseases[0].plantId = original; expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("nonexistent-plant"))).toBe(true); }); it("detects invalid causalAgentType", () => { const original = diseases[0].causalAgentType; (diseases[0] as any).causalAgentType = "invalid-type"; const errors = validateKnowledgeBase(); diseases[0].causalAgentType = original; expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("invalid-type"))).toBe(true); }); it("detects invalid severity", () => { const original = diseases[0].severity; (diseases[0] as any).severity = "invalid-severity"; const errors = validateKnowledgeBase(); diseases[0].severity = original; expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("invalid-severity"))).toBe(true); }); it("detects too few symptoms", () => { const original = [...diseases[0].symptoms]; diseases[0].symptoms = ["only one"]; const errors = validateKnowledgeBase(); diseases[0].symptoms = original; expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("fewer than 3 symptoms"))).toBe(true); }); it("detects too few causes", () => { const original = [...diseases[0].causes]; diseases[0].causes = ["only one"]; const errors = validateKnowledgeBase(); diseases[0].causes = original; expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("fewer than 2 causes"))).toBe(true); }); it("detects too few treatments", () => { const original = [...diseases[0].treatment]; diseases[0].treatment = ["one", "two"]; const errors = validateKnowledgeBase(); diseases[0].treatment = original; expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("fewer than 3 treatment"))).toBe(true); }); it("detects too few prevention tips", () => { const original = [...diseases[0].prevention]; diseases[0].prevention = ["only one"]; const errors = validateKnowledgeBase(); diseases[0].prevention = original; expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("fewer than 2 prevention"))).toBe(true); }); it("detects invalid lookalike references", () => { const original = [...diseases[0].lookalikeDiseaseIds]; diseases[0].lookalikeDiseaseIds = ["nonexistent-disease"]; const errors = validateKnowledgeBase(); diseases[0].lookalikeDiseaseIds = original; expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("nonexistent-disease"))).toBe(true); }); it("detects non-bidirectional lookalike references", () => { // early-blight references septoria-leaf-spot and late-blight // If we remove early-blight from septoria's lookalikes, it should flag const septoria = diseases.find((d) => d.id === "septoria-leaf-spot"); if (septoria) { const original = [...septoria.lookalikeDiseaseIds]; septoria.lookalikeDiseaseIds = septoria.lookalikeDiseaseIds.filter( (id) => id !== "early-blight" ); const errors = validateKnowledgeBase(); septoria.lookalikeDiseaseIds = original; expect(errors.some((e) => e.includes("not bidirectional"))).toBe(true); } }); }); describe("Data quality checks", () => { it("every disease has ≥3 symptoms", () => { for (const d of diseases) { expect(d.symptoms.length).toBeGreaterThanOrEqual(3); } }); it("every disease has ≥2 causes", () => { for (const d of diseases) { expect(d.causes.length).toBeGreaterThanOrEqual(2); } }); it("every disease has ≥3 treatment steps", () => { for (const d of diseases) { expect(d.treatment.length).toBeGreaterThanOrEqual(3); } }); it("every disease has ≥2 prevention tips", () => { for (const d of diseases) { expect(d.prevention.length).toBeGreaterThanOrEqual(2); } }); it("every disease references a valid plant ID", () => { const plantIds = new Set(plants.map((p) => p.id)); for (const d of diseases) { expect(plantIds.has(d.plantId)).toBe(true); } }); it("every disease has valid causalAgentType enum value", () => { const validTypes = ["fungal", "bacterial", "viral", "environmental"]; for (const d of diseases) { expect(validTypes).toContain(d.causalAgentType); } }); it("every disease has valid severity enum value", () => { const validSeverities = ["low", "moderate", "high", "critical"]; for (const d of diseases) { expect(validSeverities).toContain(d.severity); } }); it("all lookalike references are valid disease IDs", () => { const diseaseIds = new Set(diseases.map((d) => d.id)); for (const d of diseases) { for (const lookalikeId of d.lookalikeDiseaseIds) { expect(diseaseIds.has(lookalikeId)).toBe(true); } } }); it("includes both biotic and abiotic disease types", () => { const types = new Set(diseases.map((d) => d.causalAgentType)); expect(types.has("fungal")).toBe(true); expect(types.has("bacterial")).toBe(true); expect(types.has("viral")).toBe(true); expect(types.has("environmental")).toBe(true); }); it("has multiple plants represented", () => { const plantIds = new Set(diseases.map((d) => d.plantId)); expect(plantIds.size).toBeGreaterThanOrEqual(20); }); });