const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");

function loadWinsRenderHarness() {
  const root = path.resolve(__dirname, "..");
  const ctx = {
    console,
    window: {},
    document: {
      body: {
        classList: {
          add() {},
          remove() {},
          toggle() {}
        }
      }
    }
  };
  ctx.globalThis = ctx;
  vm.createContext(ctx);
  [
    "js/config.js",
    "js/mock-data.js",
    "js/app-state.js",
    "js/business-logic.js",
    "js/wins-bubble-math.js",
    "js/render.js"
  ].forEach(file => {
    const absolute = path.join(root, file);
    const source = file.endsWith("render.js")
      ? `${fs.readFileSync(absolute, "utf8")}\nglobalThis.__winsMapHarness = { visibleWinsForMap, winMapRegion, realWinCategories, state, bubbleMarkup, wins, winAvatarUrl, defaultWinAvatars, winCategoryShortLabel, winCategoryFilterLabel, findCommunityMapWin };`
      : fs.readFileSync(absolute, "utf8");
    vm.runInContext(source, ctx, { filename: file });
  });
  ctx.__winsMapHarness.state.wins = vm.runInContext("seedWins", ctx);
  return ctx.__winsMapHarness;
}

function categoryCounts(items) {
  return items.reduce((counts, item) => {
    counts[item.category] = (counts[item.category] || 0) + 1;
    return counts;
  }, {});
}

test("Wins map balances rendered bubbles across real categories with prototype fillers", () => {
  const harness = loadWinsRenderHarness();
  harness.state.filter = "All";
  const realWins = harness.state.wins.filter(win => !win.isHidden);
  const mapWins = harness.visibleWinsForMap(realWins, false);
  const counts = categoryCounts(mapWins);
  const realCategories = harness.realWinCategories();
  const countValues = realCategories.map(category => counts[category]);

  assert.equal(new Set(countValues).size, 1);
  assert.equal(countValues[0], 4);
  assert.ok(mapWins.every(win => realCategories.includes(win.category)));
  assert.ok(mapWins.filter(win => win.isVisualOnly).every(win => win.id.startsWith("map-filler-")));
});

test("Wins map category filter only adds fillers for the selected real category", () => {
  const harness = loadWinsRenderHarness();
  harness.state.filter = "Protein Win";
  const proteinWins = harness.state.wins.filter(win => !win.isHidden && win.category === "Protein Win");
  const mapWins = harness.visibleWinsForMap(proteinWins, false);

  assert.equal(mapWins.length, 4);
  assert.ok(mapWins.every(win => win.category === "Protein Win"));
  assert.equal(mapWins.filter(win => win.isVisualOnly).length, 3);
});

test("Wins map category colors are deterministic category mappings, not random accents", () => {
  const harness = loadWinsRenderHarness();
  const expectedAccents = {
    "Pounds Lost": "green",
    "Non-Scale Victory": "purple",
    "Protein Win": "gold",
    "Water Win": "teal",
    "Movement": "blue",
    "Meal Hack": "gold",
    "First Month": "teal",
    "Motivation": "purple",
    "Maintenance": "green",
    "Getting Started": "teal"
  };

  Object.entries(expectedAccents).forEach(([category, accent]) => {
    assert.equal(harness.winMapRegion(category).accent, accent);
  });
});

test("Wins map assigns deterministic default avatars without changing category colors", () => {
  const harness = loadWinsRenderHarness();
  const win = { id: "avatar-test", headline: "Morning walks", category: "Movement", displayName: "Mia" };
  const again = { ...win };
  const avatar = harness.winAvatarUrl(win);

  assert.equal(harness.defaultWinAvatars.length, 14);
  assert.equal(avatar, harness.winAvatarUrl(again));
  assert.match(avatar, /^assets\/avatars\/win-avatar-\d{2}\.png$/);
  assert.equal(harness.winMapRegion(win.category).accent, "blue");
});

test("Wins map honors future user avatar choices when safely URL-like", () => {
  const harness = loadWinsRenderHarness();
  const avatarUrl = "https://example.test/avatar.png";
  assert.equal(harness.winAvatarUrl({ id: "a", avatarUrl }), avatarUrl);
});

test("Wins map real bubble markup uses full-surface avatar and short win label", () => {
  const harness = loadWinsRenderHarness();
  const markup = harness.bubbleMarkup([{
    id: "content-system",
    headline: "Protein breakfast",
    details: "Started with protein.",
    category: "Protein Win",
    displayName: "Leaner member",
    weekNumber: 3,
    imageUrl: "",
    cheers: 12,
    isFeatured: false,
    isHidden: false
  }], true);

  assert.match(markup, /win-map-bubble win-map-gold photo/);
  assert.match(markup, /win-map-photo/);
  assert.match(markup, /assets\/avatars\/win-avatar-/);
  assert.match(markup, /Protein breakfast/);
  assert.match(markup, /12 cheers/);
  assert.doesNotMatch(markup, /win-map-avatar/);
  assert.doesNotMatch(markup, /win-map-icon/);
});

test("Wins map visual filler bubbles stay clean text/category cells", () => {
  const harness = loadWinsRenderHarness();
  const markup = harness.bubbleMarkup([{
    id: "text-cell",
    headline: "Better sleep",
    category: "Non-Scale Victory",
    displayName: "Prototype",
    weekNumber: 5,
    cheers: 0,
    isVisualOnly: true
  }], true);

  assert.match(markup, /<button class="win-map-bubble win-map-purple text/);
  assert.match(markup, /data-win="text-cell"/);
  assert.match(markup, /Prototype community win/);
  assert.match(markup, /Better sleep/);
  assert.match(markup, /win-map-text-mark/);
  assert.doesNotMatch(markup, /win-map-photo/);
  assert.doesNotMatch(markup, /assets\/avatars\/win-avatar-/);
});

test("Wins map prototype filler bubbles can resolve to local detail records", () => {
  const harness = loadWinsRenderHarness();
  const filler = harness.findCommunityMapWin("map-filler-protein-breakfast");

  assert.equal(filler.headline, "Protein breakfast");
  assert.equal(filler.category, "Protein Win");
  assert.equal(filler.isVisualOnly, true);
});

test("Wins map category filters use compact labels with full accessible category values", () => {
  const harness = loadWinsRenderHarness();
  harness.state.filter = "All";
  const markup = harness.wins();

  assert.equal(harness.winCategoryFilterLabel("Pounds Lost"), "Lbs Lost");
  assert.equal(harness.winCategoryFilterLabel("Non-Scale Victory"), "NSV");
  assert.equal(harness.winCategoryFilterLabel("Protein Win"), "Protein");
  assert.match(markup, /class="filters wins-map-filters" aria-label="Filter wins by category"/);
  assert.match(markup, /data-filter="Pounds Lost" aria-label="Pounds Lost" title="Pounds Lost">Lbs Lost/);
  assert.match(markup, /data-filter="Non-Scale Victory" aria-label="Non-Scale Victory" title="Non-Scale Victory">NSV/);
  assert.match(markup, /data-filter="Protein Win" aria-label="Protein Win" title="Protein Win">Protein/);
});

test("Global Leaner brand uses the screenshot palette as the approved source of truth", () => {
  const root = path.resolve(__dirname, "..");
  const html = fs.readFileSync(path.join(root, "leaner-dashboard.html"), "utf8");
  const renderSource = fs.readFileSync(path.join(root, "js/render.js"), "utf8");

  assert.match(html, /--leaner-electric: #00FB00;/);
  assert.match(html, /--leaner-plum: #3F3B57;/);
  assert.match(html, /--leaner-blue: #9FB8C5;/);
  assert.match(html, /--leaner-teal: #88EDB8;/);
  assert.match(html, /--leaner-mint: #B8F694;/);
  assert.match(html, /--leaner-lavender: #C6C1E2;/);
  assert.match(html, /--leaner-border: #C6DDD7;/);
  assert.match(html, /--leaner-mint-soft: #DFF9F6;/);
  assert.match(html, /--leaner-surface: #F1EEE4;/);
  assert.match(html, /--leaner-soft-gold: #F9F871;/);
  assert.match(html, /--leaner-bg: #FDFBE2;/);
  assert.match(html, /--primary-accent: var\(--leaner-plum\);/);
  assert.match(html, /--button-bg: var\(--leaner-plum\);/);
  assert.match(html, /--active-nav-bg: rgba\(var\(--leaner-lavender-rgb\), \.32\);/);
  assert.match(html, /<div class="brand-mark"><img src="assets\/leaner-logo\.png" alt=""><\/div>/);
  assert.match(renderSource, /hero-brand-mark"><img src="assets\/leaner-logo\.png" alt="">/);
  assert.match(renderSource, /wins-title-mark" aria-hidden="true"><img src="assets\/leaner-logo\.png" alt="">/);
  assert.doesNotMatch(html, /#403B55|#2F9E85|#D6B45D|#B97D8B|#38384D|#6C6587|#3A9B83|#D8B85A|#D98578/);
  assert.doesNotMatch(html, /204,\s*195,\s*239|248,\s*218,\s*115|159,\s*200,\s*235|107,\s*98,\s*141|24,\s*23,\s*36|78,\s*57,\s*13|255,\s*252,\s*246|253,\s*251,\s*246|232,\s*229,\s*242|244,\s*250,\s*255|56,\s*56,\s*77|108,\s*101,\s*135|58,\s*155,\s*131|216,\s*184,\s*90|217,\s*133,\s*120/);
  assert.doesNotMatch(renderSource, />L<\/span>/);
});

test("Home dashboard polish uses semantic progress color, modern CTA, and nav clearance", () => {
  const root = path.resolve(__dirname, "..");
  const html = fs.readFileSync(path.join(root, "leaner-dashboard.html"), "utf8");
  const renderSource = fs.readFileSync(path.join(root, "js/render.js"), "utf8");

  assert.match(html, /--progress-accent: var\(--leaner-teal\);/);
  assert.match(html, /--milestone-accent: var\(--leaner-soft-gold\);/);
  assert.match(html, /--community-accent: var\(--leaner-coral\);/);
  assert.match(html, /conic-gradient\(var\(--progress-accent\)/);
  assert.match(html, /\.checkin-cta::after/);
  assert.match(html, /\.action-row[\s\S]*border-radius: 18px;/);
  assert.match(html, /\.bottom-nav[\s\S]*border-radius: 24px;/);
  assert.match(html, /padding: 14px 14px calc\(142px \+ env\(safe-area-inset-bottom\)\);/);
  assert.match(html, /padding: 10px 12px calc\(156px \+ env\(safe-area-inset-bottom\)\);/);
  assert.match(renderSource, /home-progress-number/);
  assert.match(renderSource, /class="primary checkin-cta" data-route="checkin">Check in now/);
});

test("Home Wins Board preview renders as a compact cropped community map", () => {
  const root = path.resolve(__dirname, "..");
  const html = fs.readFileSync(path.join(root, "leaner-dashboard.html"), "utf8");
  const renderSource = fs.readFileSync(path.join(root, "js/render.js"), "utf8");

  assert.match(renderSource, /const previewWins = visibleWinsForMap\(visibleWins\(\)\);/);
  assert.match(renderSource, /home-wins-preview-panel/);
  assert.match(renderSource, /<h2>Wins Board<\/h2>/);
  assert.match(renderSource, /Patients choose what to share/);
  assert.match(renderSource, /aria-label="Preview of the Leaner Wins Board"/);
  assert.match(renderSource, /function winMapCategoryCenter\(category, compact = false\)/);
  assert.match(renderSource, /bubbleMarkup\(previewWins, true\)/);
  assert.match(renderSource, /const mapOffset = compact \? \(state\.winPreviewMapOffset \|\| \{ x: 0, y: 0 \}\) : \(state\.winMapOffset \|\| \{ x: 0, y: 0 \}\);/);
  assert.match(renderSource, /class="wins-preview-map-cta" data-route="wins">Explore community wins/);
  assert.match(html, /\.wins-board\.preview[\s\S]*linear-gradient\(155deg, var\(--leaner-plum\)/);
  assert.match(html, /\.wins-board\.preview::before/);
  assert.match(html, /\.wins-preview-map-cta/);
  assert.match(html, /\.home-wins-preview-heading \.button-row/);
  assert.match(html, /\.wins-map-world\.compact[\s\S]*scale\(\.74\)/);
  assert.match(html, /\.wins-board\.preview \.wins-map-world\.compact \{[\s\S]*var\(--map-x, 0px\)[\s\S]*var\(--map-y, 0px\)[\s\S]*scale\(\.74\);[\s\S]*\}/);
  assert.match(html, /@media \(max-width: 759px\)[\s\S]*\.wins-map-world\.compact \{ transform: translate\(-50%, -50%\) scale\(\.68\); \}/);
  assert.match(html, /@media \(max-width: 759px\)[\s\S]*\.wins-board\.preview \.wins-map-world\.compact \{ transform: translate\(calc\(-50% - 96px \+ var\(--map-x, 0px\)\), calc\(-50% - 136px \+ var\(--map-y, 0px\)\)\) scale\(\.68\);[\s\S]*\}/);
});

test("Leaner logo renders as contained full-mark badges instead of cropped squares", () => {
  const root = path.resolve(__dirname, "..");
  const html = fs.readFileSync(path.join(root, "leaner-dashboard.html"), "utf8");

  assert.match(html, /\.brand-mark[\s\S]*width: 92px;[\s\S]*height: 46px;[\s\S]*padding: 6px 8px;/);
  assert.match(html, /\.brand-mark img[\s\S]*max-width: 100%;[\s\S]*max-height: 34px;[\s\S]*object-fit: contain;/);
  assert.match(html, /\.hero-brand-mark[\s\S]*width: min\(150px, 46vw\);[\s\S]*height: 88px;[\s\S]*padding: 10px 14px;/);
  assert.match(html, /\.hero-brand-mark img[\s\S]*max-height: 68px;/);
  assert.match(html, /\.wins-title-mark img[\s\S]*max-height: 28px;/);
  assert.doesNotMatch(html, /\.brand-mark[\s\S]{0,220}object-fit: cover/);
  assert.match(html, /@media \(max-width: 759px\)[\s\S]*\.brand-mark[\s\S]*width: 76px;[\s\S]*height: 40px;/);
});

test("Wins map motion uses rAF focus scheduling and soft membrane transitions", () => {
  const root = path.resolve(__dirname, "..");
  const events = fs.readFileSync(path.join(root, "js/events.js"), "utf8");
  const html = fs.readFileSync(path.join(root, "leaner-dashboard.html"), "utf8");

  assert.match(events, /const requestFocusUpdate = \(\) =>/);
  assert.match(events, /focusFrame = requestAnimationFrame/);
  assert.match(events, /requestFocusUpdate\(\);/);
  assert.doesNotMatch(events, /bubble\.style\.setProperty\("--focus-scale", "1"\);/);
  assert.match(html, /\.wins-board\.panning \.win-map-bubble/);
  assert.match(html, /cubic-bezier\(\.18, \.82, \.24, 1\)/);
});

test("Wins map card supports tap detail and in-card cheering without drag opens", () => {
  const root = path.resolve(__dirname, "..");
  const events = fs.readFileSync(path.join(root, "js/events.js"), "utf8");
  const html = fs.readFileSync(path.join(root, "leaner-dashboard.html"), "utf8");

  assert.match(events, /openWin\(btn\.dataset\.win\)/);
  assert.match(events, /makeWinMapPannable\(document\.getElementById\("previewBoard"\), \{ compact: true, stateKey: "winPreviewMapOffset" \}\)/);
  assert.match(events, /const world = options\.compact \? board\?\.querySelector\("\.wins-map-world\.compact"\) : board\?\.querySelector\("\.wins-map-world:not\(\.compact\)"\);/);
  assert.match(events, /const offsetKey = options\.stateKey \|\| "winMapOffset";/);
  assert.match(events, /state\[offsetKey\] = nextOffset;/);
  assert.match(events, /const isCompactMap = world\.classList\.contains\("compact"\);/);
  assert.match(events, /maxScale: measured\.id === focusedId \? \(isCompactMap \? 1\.24 : 1\.56\) : \(isCompactMap \? 1\.13 : 1\.34\)/);
  assert.match(events, /minGap: isCompactMap \? 14 : 10/);
  assert.match(events, /dataset\.suppressClick/);
  assert.match(events, /dataset\.pointerIntent = "pan"/);
  assert.match(events, /if \(!moved && targetWin\?\.dataset\.win\)/);
  assert.match(events, /openWin\(targetWin\.dataset\.win\)/);
  assert.match(events, /const realWin = findWin\(id\)/);
  assert.match(events, /findCommunityMapWin\(id\)/);
  assert.match(events, /prototypeWinDetails/);
  assert.match(events, /if \(!isPrototypeWin\) \{/);
  assert.match(events, /const imageUrl = isUsableImageUrl\(w\.imageUrl\) \? w\.imageUrl : winAvatarUrl\(w\)/);
  assert.match(events, /class="modal-card win-detail-card"/);
  assert.match(events, /class="win-detail-story"/);
  assert.match(events, /data-cheer-count/);
  assert.match(events, /data-cheer-copy/);
  assert.match(events, /cheerButton\.innerHTML = `\$\{icon\("heart"\)\} Cheered`/);
  assert.doesNotMatch(events, /cheerBtn"\)\.addEventListener\("click", \(\) => \{ w\.cheers \+= 1; awardRule\("celebrate_win", "Celebrated a community win"\); save\(\); modal\.classList\.remove\("open"\); render\(\); \}\);/);
  assert.match(html, /\.win-detail-card/);
  assert.match(html, /\.win-detail-stats/);
  assert.match(html, /render\.js\?v=weekly-checkin-chips-1/);
  assert.match(html, /events\.js\?v=weekly-checkin-chips-1/);
});

test("Weekly Check-In uses emoji choice chips while preserving submit fields", () => {
  const root = path.resolve(__dirname, "..");
  const renderSource = fs.readFileSync(path.join(root, "js/render.js"), "utf8");
  const eventsSource = fs.readFileSync(path.join(root, "js/events.js"), "utf8");
  const html = fs.readFileSync(path.join(root, "leaner-dashboard.html"), "utf8");

  assert.match(renderSource, /Overall, I&apos;m feeling/);
  assert.match(renderSource, /name="\$\{name\}" value="\$\{value\}"/);
  assert.match(renderSource, /choice\("hunger", "1", "&#128578;", "Low"\)/);
  assert.match(renderSource, /choice\("hunger", "2", "&#128524;", "Manageable", true\)/);
  assert.match(renderSource, /choice\("hunger", "4", "&#127384;", "Hard to control"\)/);
  assert.match(renderSource, /choice\("energy", "1", "&#128267;", "Low"\)/);
  assert.match(renderSource, /choice\("energy", "4", "&#9889;", "Great", true\)/);
  assert.match(renderSource, /choice\("sideEffects", "needs_help", "&#9888;", "Yes, I need help"\)/);
  assert.match(renderSource, /name="sideEffectsNote"/);
  assert.match(renderSource, /Submit my check-in/);
  assert.doesNotMatch(renderSource, /name="hunger" type="range"/);
  assert.doesNotMatch(renderSource, /name="energy" type="range"/);
  assert.doesNotMatch(renderSource, /<select name="sideEffects"/);

  assert.match(html, /\.checkin-choice:has\(input:checked\)/);
  assert.match(html, /\.side-effect-group:has\(input\[value="mild"\]:checked\) \+ \.side-effect-note/);
  assert.match(html, /\.side-effect-grid \.checkin-choice:last-child/);

  assert.match(eventsSource, /const sideEffects = String\(fd\.get\("sideEffects"\) \|\| "no"\)/);
  assert.match(eventsSource, /feeling: fd\.get\("feeling"\)/);
  assert.match(eventsSource, /sideEffectsNote: fd\.get\("sideEffectsNote"\)/);
  assert.match(eventsSource, /helpNeeded: fd\.get\("helpNeeded"\)/);
  assert.match(eventsSource, /needsClinicFollowup: sideEffects === "needs_help"/);
});
