import { describe, expect, it } from "vitest";
import { analyzeReference } from "./analyze.js";
import { colorDistance } from "./Raster.js";
import { denoise, segmentForeground } from "./segment.js";
import { DEFAULT_FIGURE, PACKED_FIGURE, renderFigure } from "./testFixtures.js";

/**
 * Ground truth is known here, so tolerances are tight where the analyzer
 * genuinely measures and loose where it admits to deriving.
 */

function analyze(spec = DEFAULT_FIGURE) {
  const raster = renderFigure(spec);
  const mask = denoise(segmentForeground(raster));
  return { ...analyzeReference(raster, mask), mask };
}

/** Landmarks are normalised to the *body*, the fixture to the *image*. */
function toBodySpace(spec: typeof DEFAULT_FIGURE, imageFraction: number): number {
  return (imageFraction - spec.headTop) / (spec.soleY - spec.headTop);
}

describe("segmentation", () => {
  it("separates the figure from a flat backdrop", () => {
    const { mask } = analyze();
    expect(mask.coverage).toBeGreaterThan(0.05);
    expect(mask.coverage).toBeLessThan(0.6);
  });

  it("keeps a backdrop-coloured patch enclosed by the body", () => {
    // A grey badge in the middle of the torso, the exact colour of the
    // backdrop. A global colour threshold would punch a hole through the
    // character; a flood fill from the border cannot reach it.
    const spec = DEFAULT_FIGURE;
    const raster = renderFigure(spec);

    const midX = Math.round(spec.width / 2);
    const torsoY = Math.round(((spec.shoulderY + spec.crotchY) / 2) * spec.height);
    for (let y = torsoY - 12; y <= torsoY + 12; y++) {
      for (let x = midX - 12; x <= midX + 12; x++) {
        const i = (y * spec.width + x) * 4;
        raster.data[i] = spec.backdrop.r;
        raster.data[i + 1] = spec.backdrop.g;
        raster.data[i + 2] = spec.backdrop.b;
      }
    }

    const mask = denoise(segmentForeground(raster));
    expect(mask.data[torsoY * mask.width + midX]).toBe(1);
  });
});

describe("analyzeReference landmarks", () => {
  it("measures the neck near the head/body junction", () => {
    const { measurements } = analyze();
    expect(measurements.landmarks.neck.y).toBeCloseTo(
      toBodySpace(DEFAULT_FIGURE, DEFAULT_FIGURE.neckY),
      1,
    );
  });

  it("measures the crotch where the legs separate", () => {
    const { measurements } = analyze();
    expect(measurements.landmarks.crotch.source).toBe("measured");
    expect(measurements.landmarks.crotch.y).toBeCloseTo(
      toBodySpace(DEFAULT_FIGURE, DEFAULT_FIGURE.crotchY),
      1,
    );
  });

  it("measures the ankle above the boot flare", () => {
    const { measurements } = analyze();
    expect(measurements.landmarks.ankle.y).toBeGreaterThan(0.85);
    expect(measurements.landmarks.ankle.y).toBeLessThan(0.98);
  });

  it("orders every landmark head to toe", () => {
    const { landmarks } = analyze().measurements;
    const order = [
      landmarks.neck.y,
      landmarks.shoulder.y,
      landmarks.chest.y,
      landmarks.waist.y,
      landmarks.hip.y,
      landmarks.crotch.y,
      landmarks.knee.y,
      landmarks.ankle.y,
    ];
    for (let i = 1; i < order.length; i++) {
      expect(order[i]).toBeGreaterThanOrEqual(order[i - 1]!);
    }
  });
});

describe("analyzeReference robustness", () => {
  it("rejects an implausible shoulder caused by worn gear", () => {
    // The pack merges with the collar, so the width test fires under the jaw.
    // The analyzer must notice and fall back rather than rig arms to the chin.
    const { measurements, notes } = analyze(PACKED_FIGURE);

    expect(measurements.landmarks.shoulder.source).toBe("derived");
    expect(notes.join(" ")).toContain("plausible band");
    // Still lands somewhere a shoulder could actually be.
    expect(measurements.landmarks.shoulder.y).toBeGreaterThan(0.12);
    expect(measurements.landmarks.shoulder.y).toBeLessThan(0.3);
  });

  it("keeps the torso width free of the arms", () => {
    const { measurements } = analyze();
    // Torso is 0.26 of image height; arms add ~0.15 per side if wrongly merged.
    const expected = DEFAULT_FIGURE.torsoWidth / (DEFAULT_FIGURE.soleY - DEFAULT_FIGURE.headTop);
    expect(measurements.widths.chest).toBeLessThan(expected * 1.4);
  });

  it("reports a thigh wider than a calf", () => {
    const { measurements } = analyze();
    expect(measurements.widths.thigh).toBeGreaterThan(0);
    expect(measurements.widths.calf).toBeGreaterThan(0);
  });

  it("survives a figure whose legs never separate", () => {
    const { measurements, notes } = analyze({ ...DEFAULT_FIGURE, legGap: 0 });
    expect(measurements.landmarks.crotch.y).toBeGreaterThan(0.3);
    expect(measurements.landmarks.crotch.y).toBeLessThan(0.75);
    if (measurements.landmarks.crotch.source === "derived") {
      expect(notes.join(" ")).toContain("crotch");
    }
  });
});

describe("palette sampling", () => {
  it("recovers the colour of each body region", () => {
    const { palette } = analyze();
    const { colors } = DEFAULT_FIGURE;

    // Generous tolerance: the sampler insets from edges and takes a median.
    expect(colorDistance(palette.hair, colors.hair)).toBeLessThan(40);
    expect(colorDistance(palette.skin, colors.skin)).toBeLessThan(40);
    expect(colorDistance(palette.torsoUpper, colors.torso)).toBeLessThan(40);
    expect(colorDistance(palette.leg, colors.leg)).toBeLessThan(40);
    expect(colorDistance(palette.foot, colors.foot)).toBeLessThan(40);
  });

  it("distinguishes the arms from the torso", () => {
    const { palette } = analyze();
    expect(colorDistance(palette.arm, DEFAULT_FIGURE.colors.arm)).toBeLessThan(45);
  });

  it("never returns the backdrop colour for a body region", () => {
    const { palette } = analyze();
    for (const [region, color] of Object.entries(palette)) {
      expect(
        colorDistance(color, DEFAULT_FIGURE.backdrop),
        `${region} sampled the backdrop`,
      ).toBeGreaterThan(8);
    }
  });
});

describe("analyzeReference on a real generated reference", () => {
  // The image Grok's image_gen actually produced: a survivor in a bulky jacket
  // wearing a high pack. Synthetic fixtures cannot reproduce JPEG ringing,
  // painted shading, or gear that genuinely merges with the collar.
  const fixture = new URL("../../fixtures/survivor-reference.jpg", import.meta.url).pathname;

  it("produces an anatomically ordered figure", async () => {
    const { loadRaster } = await import("./Raster.js");
    const raster = await loadRaster(fixture);
    const mask = denoise(segmentForeground(raster));
    const { measurements, palette } = analyzeReference(raster, mask);
    const { landmarks, widths } = measurements;

    expect(landmarks.neck.y).toBeGreaterThan(0.08);
    expect(landmarks.neck.y).toBeLessThan(0.2);
    // Must not be dragged up to the arm-separation line by the loose jacket.
    expect(landmarks.crotch.y).toBeGreaterThan(0.5);
    expect(landmarks.crotch.y).toBeLessThan(0.62);
    expect(landmarks.ankle.y).toBeGreaterThan(0.85);

    // Torso tapers downward, and never includes the arms.
    expect(widths.chest).toBeGreaterThan(widths.hip);
    expect(widths.chest).toBeLessThan(0.35);
    expect(widths.thigh).toBeGreaterThan(widths.calf);

    // The jacket is olive and the trousers are dark; they must not be confused.
    expect(palette.torsoUpper.g).toBeGreaterThan(palette.leg.g);
  }, 30_000);
});
