// Mocks the API via cy.intercept — this job doesn't run a live backend (see
// .github/workflows/ci.yml); apps/api's own Mocha suite covers real API
// behavior against a real database.
//
// Pure layout/style specs — plain Cypress, no Cucumber (see the 3-way test
// split: journeys via Gherkin, generic components via Component Testing,
// and this file's category: the app *shell*'s own structural/visual
// contract, independent of any particular page's content or user journey).
// Regression coverage for the two bugs fixed in #21: the sidebar scrolling
// away with a tall page, and pages not consistently using the available
// width (some stuck to the left edge with a lopsided gap, others correctly
// full-bleed).

const authenticatedProfile = {
  id: 1,
  firstName: "Alice",
  lastName: "Martin",
  email: "alice@example.com",
  tokenVersion: 0,
  houseId: 1,
  dietId: null,
};

function interceptAuth() {
  cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
}

describe("App shell — sidebar is a fixed-width rail on every page", () => {
  beforeEach(() => {
    interceptAuth();
  });

  it("keeps the sidebar at its full expanded width (15rem = 240px), regardless of the page", () => {
    cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
    cy.visit("/");
    cy.get(".app-sidebar")
      .should(($sidebar) => {
        expect($sidebar[0].getBoundingClientRect().width).to.be.closeTo(240, 1);
      })
      // Flush to the top-left corner of the viewport — nothing pushes it
      // down or in, on any page.
      .and(($sidebar) => {
        const rect = $sidebar[0].getBoundingClientRect();
        expect(rect.top).to.equal(0);
        expect(rect.left).to.equal(0);
      });

    cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
    cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
    cy.visit("/recettes");
    cy.get(".app-sidebar").should(($sidebar) => {
      expect($sidebar[0].getBoundingClientRect().width).to.be.closeTo(240, 1);
    });
  });

  it("shrinks to the icon-only rail width (4.25rem = 68px) once collapsed, and restores 240px when expanded again", () => {
    cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
    cy.visit("/");

    cy.get(".app-sidebar").should(($sidebar) => {
      expect($sidebar[0].getBoundingClientRect().width).to.be.closeTo(240, 1);
    });

    cy.get(".app-sidebar__collapse-toggle").click();
    cy.get(".app-sidebar").should(($sidebar) => {
      expect($sidebar[0].getBoundingClientRect().width).to.be.closeTo(68, 1);
    });

    cy.get(".app-sidebar__collapse-toggle").click();
    cy.get(".app-sidebar").should(($sidebar) => {
      expect($sidebar[0].getBoundingClientRect().width).to.be.closeTo(240, 1);
    });
  });
});

describe("App shell — viewport-locked height, independent scroll (#21 regression)", () => {
  beforeEach(() => {
    interceptAuth();
  });

  it("pins the whole shell to exactly the viewport height, never taller", () => {
    cy.viewport(1200, 700);
    cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
    cy.visit("/");

    cy.get(".app-layout").should(($layout) => {
      expect($layout[0].getBoundingClientRect().height).to.be.closeTo(700, 1);
      expect(getComputedStyle($layout[0]).overflow).to.equal("hidden");
    });
    // The document itself never grows past the viewport — this is the exact
    // root cause of the original bug (a tall page scrolling the whole
    // document, dragging the sidebar along with it).
    cy.document().its("documentElement.scrollHeight").should("be.closeTo", 700, 1);
  });

  it("scrolls only the content area on a page taller than the viewport — the sidebar never moves and the document itself doesn't scroll", () => {
    cy.viewport(1200, 700);
    cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
    cy.visit("/");

    // A synthetic spacer, far taller than the viewport — deliberately
    // independent of whatever the planning page's own content happens to
    // be, since this test is about the shell's scroll contract, not this
    // particular page's height.
    cy.get(".app-content").then(($content) => {
      const spacer = document.createElement("div");
      spacer.style.height = "3000px";
      spacer.setAttribute("data-cy", "scroll-spacer");
      $content[0].appendChild(spacer);
    });

    cy.get(".app-sidebar").then(($sidebar) => {
      const topBefore = $sidebar[0].getBoundingClientRect().top;

      cy.get(".app-content").scrollTo("bottom");

      cy.get(".app-sidebar").should(($again) => {
        expect($again[0].getBoundingClientRect().top).to.equal(topBefore);
      });
    });

    // The scroll genuinely happened inside `.app-content`...
    cy.get(".app-content").invoke("scrollTop").should("be.greaterThan", 0);
    // ...and not on the document/window itself.
    cy.window().its("scrollY").should("equal", 0);
  });
});

describe("Page width — full-bleed pages vs. centered reading columns (#21 regression)", () => {
  beforeEach(() => {
    interceptAuth();
    cy.viewport(1600, 900);
  });

  it("stretches the planning page, recipe catalog, and settings pages across the full content width", () => {
    cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
    cy.visit("/");
    assertFillsContentWidth(".planning-page");

    cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
    cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
    cy.visit("/recettes");
    assertFillsContentWidth(".recipes-page");

    // Settings pages (Compte/Préférences/Foyer) used to be capped at
    // max-width: 56rem, cramped in a centered column on wide viewports —
    // that cap was dropped so they fill the width like every other page.
    cy.visit("/parametres/compte");
    assertFillsContentWidth(".settings-page");

    cy.intercept("GET", /\/shopping-list\?/, {
      statusCode: 200,
      body: { startDate: "2026-08-17", finishDate: "2026-08-23", items: [] },
    });
    cy.visit("/liste-de-courses");
    assertFillsContentWidth(".shopping-list-page");
  });

  /** Fills `.app-content`'s available (padding-excluded) width, within a couple px of scrollbar/rounding slack. */
  function assertFillsContentWidth(selector: string) {
    cy.get(".app-content").then(($content) => {
      const style = getComputedStyle($content[0]);
      const available =
        $content[0].getBoundingClientRect().width -
        Number.parseFloat(style.paddingLeft) -
        Number.parseFloat(style.paddingRight);

      cy.get(selector).should(($page) => {
        expect($page[0].getBoundingClientRect().width).to.be.closeTo(available, 3);
      });
    });
  }
});

describe("Responsive breakpoint — sidebar becomes a horizontal top bar under 640px", () => {
  beforeEach(() => {
    interceptAuth();
    cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
  });

  it("switches to a full-width horizontal bar, hides the collapse toggle and version tag, and keeps every nav link legible", () => {
    cy.viewport(375, 812);
    cy.visit("/");

    cy.get(".app-sidebar").should(($sidebar) => {
      const rect = $sidebar[0].getBoundingClientRect();
      expect(rect.width).to.be.closeTo(375, 1);
      // A short horizontal bar, not the tall vertical rail — well under the
      // desktop rail's own content-driven height.
      expect(rect.height).to.be.lessThan(120);
    });

    // Nothing to collapse into on a bar with no rail to shrink.
    cy.get(".app-sidebar__collapse-toggle").should("not.be.visible");
    cy.get(".app-sidebar__version").should("not.be.visible");

    // The main nav must stay fully legible and tappable — icon *and* label
    // — falling back to horizontal scroll instead of ever being crushed
    // down to unlabeled slivers (see AppLayout.scss's own comment on this
    // exact failure mode).
    cy.get(".app-sidebar__nav").should(($nav) => {
      expect(getComputedStyle($nav[0]).overflowX).to.equal("auto");
    });
    cy.contains(".app-sidebar__nav a", "Planning").find("span.label").should("be.visible");
    cy.contains(".app-sidebar__nav a", "Planning").should(($link) => {
      expect($link[0].getBoundingClientRect().width).to.be.greaterThan(40);
    });

    // Contrast: the settings/account toggles' labels *do* collapse to
    // icon-only here — there's no room for both a full nav row and full
    // text labels on every piece of chrome at once.
    cy.contains("button", "Paramètres").find("span.label").should("not.be.visible");
  });
});

describe("Color theme — light/dark tokens actually reach the rendered chrome", () => {
  beforeEach(() => {
    interceptAuth();
    cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
  });

  it("renders the sidebar surface and the active nav link in the light palette by default", () => {
    cy.intercept("GET", "**/preferences", { statusCode: 200, body: { theme: "LIGHT" } });
    cy.visit("/");

    cy.get("html").should("have.attr", "data-theme", "light");
    // --color-surface: #ffffff
    cy.get(".app-sidebar").should(($el) => {
      expect(getComputedStyle($el[0]).backgroundColor).to.equal("rgb(255, 255, 255)");
    });
    // --color-primary: #2e6b4a, applied as the active nav link's background.
    cy.contains(".app-sidebar__nav a", "Planning").should(($link) => {
      expect(getComputedStyle($link[0]).backgroundColor).to.equal("rgb(46, 107, 74)");
    });
  });

  it("switches every themed color to the dark palette when the user's preference is DARK", () => {
    cy.intercept("GET", "**/preferences", { statusCode: 200, body: { theme: "DARK" } });
    cy.visit("/");

    cy.get("html").should("have.attr", "data-theme", "dark");
    // --color-surface: #1c221e
    cy.get(".app-sidebar").should(($el) => {
      expect(getComputedStyle($el[0]).backgroundColor).to.equal("rgb(28, 34, 30)");
    });
    // --color-primary: #5fae7e
    cy.contains(".app-sidebar__nav a", "Planning").should(($link) => {
      expect(getComputedStyle($link[0]).backgroundColor).to.equal("rgb(95, 174, 126)");
    });
    // The page background token switches too, not just the sidebar.
    cy.get("body").should(($body) => {
      // --color-background: #14181a
      expect(getComputedStyle($body[0]).backgroundColor).to.equal("rgb(20, 24, 26)");
    });
  });
});
