/* project-detail.jsx — redesigned Project Detail (Direction B), wired to the
   live backend through the same `onDrawer` / `onAction` contract the original
   ProjectDetail used. Drop-in replacement: render <ProjectDetailView db,
   project, onBack, onDrawer, onAction /> wherever the old ProjectDetail was.

   Reuses existing app primitives: Panel, DataTable, Pill, Avatar, KPI, Icon,
   Toolbar, money/fmtDate (window.AC), MODEL. Planning tab lives in
   project-planning.jsx (window.ProjectPlanningExec). Lifeline + ring live in
   project-lifeline.jsx.

   NOTE: inline edits of project fields (name/kind/status/description) call
   onAction('updateProject', { projectId, project, patch }). Add that one case
   to app.jsx (see README). All other actions already exist. */
(function () {
  const { useState } = React;
  const { money, fmtDate } = window.AC;
  const MODEL = window.MODEL;

  const decimal = (v) => { const n = Number(v); return Number.isFinite(n) ? n.toString() : '0'; };
  const percent = (v) => `${decimal((Number(v) || 0) * 100)}%`;

  /* ---------- finance helpers (identical to the original cards.jsx) ---------- */
  function projectFinance(db, projectId) {
    const project = db.projects.find((p) => p.id === projectId);
    const deliverables = new Set((project ? project.projectDeliverables : []).map((s) => s.id));
    const invs = db.invoices.filter((iv) => (iv.lines || []).some((line) => line.projectId === projectId || deliverables.has(line.projectDeliverableId)));
    let billed = 0, outstanding = 0;
    invs.forEach((iv) => { const e = MODEL.enrich(db, iv); if (!MODEL.isDraft(iv) && !MODEL.isCancelled(iv)) billed += e.totals.total; if (e.open) outstanding += e.totals.outstanding; });
    return { invs, billed, outstanding };
  }
  function customerOutstanding(db, customerId) {
    let out = 0;
    db.invoices.filter((iv) => iv.customerId === customerId).forEach((iv) => { const e = MODEL.enrich(db, iv); if (e.open) out += e.totals.outstanding; });
    return out;
  }
  const offerTotals = (offer) => {
    const subtotal = (offer.lines || []).reduce((s, l) => s + l.quantity * l.unitPrice, 0);
    const tax = (offer.lines || []).reduce((s, l) => s + l.quantity * l.unitPrice * l.taxRate, 0);
    return { subtotal, tax, total: subtotal + tax };
  };
  function projectDeliverableRows(project, db) {
    const invoices = db.invoices || [];
    return [
      ...project.projectDeliverables.map((deliverable) => {
        const milestoneIds = deliverable.milestoneIds || [];
        const milestoneBillable = milestoneIds.length === 0 || milestoneIds.every((id) => (project.milestones || []).some((m) => m.id === id && m.completed));
        const existingInvoices = invoices.filter((iv) => (iv.lines || []).some((l) => l.projectDeliverableId === deliverable.id));
        const existingDates = new Set(existingInvoices.map((iv) => String(iv.issueDate).slice(0, 10)));
        const serviceSchedule = { ...deliverable, date: deliverable.billingStartDate };
        const missingInvoiceCount = deliverable.freeOfCharge || !milestoneBillable ? 0
          : deliverable.recurrenceFrequency === 'None' && existingInvoices.length ? 0
          : MODEL.expenseOccurrences(serviceSchedule).filter((d) => !existingDates.has(d)).length;
        return { type: 'deliverable', id: 'deliverable-' + deliverable.id, deliverable, missingInvoiceCount, milestoneBillable };
      }),
      ...(project.offers || []).map((offer) => ({ type: 'offer', id: 'offer-' + offer.id, offer })),
    ];
  }
  function progressOf(project) {
    const ms = project.milestones || [];
    const done = ms.filter((m) => m.completed).length;
    let tt = 0, td = 0;
    (project.planningSections || []).forEach((s) => {
      (s.todoClusters || []).forEach((cluster) => (cluster.todos || []).forEach((todo) => { tt++; if (todo.done) td++; }));
    });
    return { msDone: done, msTotal: ms.length, msPct: ms.length ? done / ms.length : 0, todoDone: td, todoTotal: tt };
  }

  /* ---------- inline editors ---------- */
  function Inline({ value, onCommit, multiline, placeholder, className, style, rows = 6 }) {
    const [editing, setEditing] = useState(false);
    const [val, setVal] = useState(value);
    React.useEffect(() => { setVal(value); }, [value]);
    const commit = () => { setEditing(false); if (val !== value) onCommit(val); };
    const cancel = () => { setVal(value); setEditing(false); };
    if (editing) {
      if (multiline) return <textarea className="inl-area" autoFocus rows={rows} value={val} style={style} onChange={(e) => setVal(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === 'Escape') cancel(); if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) commit(); }} />;
      return <input className="inl-input" autoFocus value={val} style={style} onChange={(e) => setVal(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === 'Enter') commit(); if (e.key === 'Escape') cancel(); }} />;
    }
    return <span className={'inl ' + (className || '')} tabIndex={0} style={style} onClick={() => setEditing(true)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); setEditing(true); } }}>{value ? value : <span className="faint">{placeholder || 'Add…'}</span>}</span>;
  }

  function StatusPick({ value, options, onChange }) {
    const [open, setOpen] = useState(false);
    const ref = React.useRef(null);
    React.useEffect(() => {
      if (!open) return;
      const off = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
      document.addEventListener('pointerdown', off, true);
      return () => document.removeEventListener('pointerdown', off, true);
    }, [open]);
    return (
      <span className="statuspick" ref={ref}>
        <button style={{ border: 0, background: 'transparent', padding: 0, cursor: 'pointer' }} onClick={() => setOpen((o) => !o)} title="Change status"><Pill kind={value} sm /></button>
        {open && <div className="statuspick-menu">{options.map((o) => <button key={o} onClick={() => { onChange(o); setOpen(false); }}><Pill kind={o} sm /></button>)}</div>}
      </span>
    );
  }

  function projectDocumentsFor(project, target = {}) {
    const docs = project && project.documents ? project.documents : [];
    return docs.filter((doc) =>
      (target.planningSectionId ? doc.planningSectionId === target.planningSectionId : !doc.planningSectionId)
      && (target.milestoneId ? doc.milestoneId === target.milestoneId : !doc.milestoneId)
      && (target.todoId ? doc.todoId === target.todoId : !doc.todoId));
  }

  function isFileDrag(e) {
    return Array.from((e.dataTransfer && e.dataTransfer.types) || []).includes('Files');
  }

  function uploadProjectDocumentFiles(project, onAction, target, files) {
    if (!project || !onAction || !files || !files.length) return;
    Array.from(files).forEach((file) => {
      onAction('uploadProjectDocument', { projectId: project.id, file, ...(target || {}) });
    });
  }

  function projectDocumentDropProps(project, onAction, target, setOver) {
    return {
      onDragOver: (e) => {
        if (!isFileDrag(e)) return;
        e.preventDefault();
        e.stopPropagation();
        e.dataTransfer.dropEffect = 'copy';
        if (setOver) setOver(true);
      },
      onDragLeave: (e) => {
        if (!isFileDrag(e)) return;
        if (!e.currentTarget.contains(e.relatedTarget) && setOver) setOver(false);
      },
      onDrop: (e) => {
        const files = e.dataTransfer && e.dataTransfer.files;
        if (!files || !files.length) return;
        e.preventDefault();
        e.stopPropagation();
        if (setOver) setOver(false);
        uploadProjectDocumentFiles(project, onAction, target, files);
      },
    };
  }

  function ProjectDocumentArea({ project, target, onAction, empty = 'No documents attached.', compact = false }) {
    const [over, setOver] = useState(false);
    const docs = projectDocumentsFor(project, target);
    const upload = (e) => {
      const files = e.target.files;
      e.target.value = '';
      uploadProjectDocumentFiles(project, onAction, target, files);
    };
    const row = (doc) => (
      <div key={doc.id} className="pd-docarea-row">
        <button type="button" className="chip pd-docchip" title={doc.fileName} onClick={() => onAction('openProjectDocument', { projectId: project.id, documentId: doc.id })}>
          <Icon name="doc" size={12} />{doc.fileName}
        </button>
        <button type="button" className="icon-btn" title="Remove document" onClick={() => onAction('deleteProjectDocument', { projectId: project.id, documentId: doc.id, fileName: doc.fileName })}>
          <Icon name="trash" size={13} />
        </button>
      </div>
    );
    return (
      <div className={'pd-docarea' + (compact ? ' compact' : '') + (over ? ' over' : '')} {...projectDocumentDropProps(project, onAction, target, setOver)}>
        <div className="pd-docarea-head">
          <label>Documents</label>
          <label className="btn sm ghost" style={{ cursor: 'pointer' }}><Icon name="plus" size={13} />Upload<input type="file" multiple style={{ display: 'none' }} onChange={upload} /></label>
        </div>
        <div className="pd-docarea-list">
          {docs.length ? docs.map(row) : <span className="muted" style={{ fontSize: 12 }}>{empty}</span>}
        </div>
        <div className="pd-docarea-drop"><Icon name="download" size={13} />Drop files here</div>
      </div>
    );
  }

  /* ---------- dashboard cards ---------- */
  function OverviewCard({ project, saveProject }) {
    return (
      <Panel title="Overview">
        <div className="pd-meta-row" style={{ marginBottom: 12 }}>
          <StatusPick value={project.status} options={['Active', 'Completed', 'Inactive']} onChange={(v) => saveProject({ status: v })} />
          <span className="chip"><Inline value={project.kind} onCommit={(v) => saveProject({ kind: v })} /></span>
        </div>
        <Inline className="pd-overview-desc" multiline rows={5} value={project.description} onCommit={(v) => saveProject({ description: v })} placeholder="Add a project description…" style={{ display: 'block' }} />
        {project.projectRepoUrl && <div style={{ marginTop: 13 }}><a href={project.projectRepoUrl} target="_blank" rel="noreferrer" className="chip"><Icon name="external" size={12} />Project repo</a></div>}
        {project.projectAppUrl && <div style={{ marginTop: project.projectRepoUrl ? 8 : 13 }}><a href={project.projectAppUrl} target="_blank" rel="noreferrer" className="chip"><Icon name="external" size={12} />Deployed app</a></div>}
        {project.designMdKitIdentifier && <div style={{ marginTop: 8 }}><a href={project.designMdKitUrl || `https://designmd.ai/${project.designMdKitIdentifier}`} target="_blank" rel="noreferrer" className="chip"><Icon name="doc" size={12} />{project.designMdKitName || project.designMdKitIdentifier}</a></div>}
      </Panel>
    );
  }

  function CustomersCard({ project, db, onDrawer, onAction }) {
    const custs = project.customerIds.map((id) => db.customers.find((c) => c.id === id)).filter(Boolean);
    const canLink = db.customers.some((c) => !project.customerIds.includes(c.id));
    return (
      <Panel title="Linked customers" sub={`${custs.length} linked`} actions={canLink && <button className="btn sm ghost" onClick={() => onDrawer('projectCustomer', null, { projectId: project.id })}><Icon name="plus" size={14} />Customer</button>}>
        {custs.length ? custs.map((c) => {
          const owe = customerOutstanding(db, c.id);
          return (
            <div className="pd-customer-row" key={c.id}>
              <Avatar name={c.name} />
              <div className="meta"><div className="nm">{c.name}</div><div className="em">{c.email}</div></div>
              {owe > 0 ? <span className="owe">{money(owe)} due</span> : <span className="faint" style={{ fontSize: 11 }}>Settled</span>}
              <button className="icon-btn" title="Remove customer link" onClick={() => onAction('unlinkProjectCustomer', { projectId: project.id, customerId: c.id, customerName: c.name })}><Icon name="trash" size={14} /></button>
            </div>
          );
        }) : <span className="muted" style={{ fontSize: 12 }}>No customers linked.</span>}
      </Panel>
    );
  }

  function ProgressContent({ project, ringSize = 104 }) {
    const pr = progressOf(project);
    const openMs = pr.msTotal - pr.msDone;
    return (
      <div className="ring-wrap">
        <ProgressRing value={pr.msPct} size={ringSize} label="done" />
        <div className="pd-tiles" style={{ flex: 1, gridTemplateColumns: '1fr 1fr' }}>
          <div className="pd-tile"><div className="k">Milestones</div><div className="v">{pr.msDone}/{pr.msTotal}</div></div>
          <div className="pd-tile"><div className="k">Open</div><div className="v">{openMs}</div></div>
          <div className="pd-tile"><div className="k">Todos done</div><div className="v">{pr.todoDone}/{pr.todoTotal}</div></div>
          <div className="pd-tile"><div className="k">Sections</div><div className="v">{(project.planningSections || []).length}</div></div>
        </div>
      </div>
    );
  }

  /* ---------- Documents (collapsible) ---------- */
  function DocumentsPanel({ project, onAction }) {
    const [open, setOpen] = useState(false);
    const [targetValue, setTargetValue] = useState('project');
    const sections = project.planningSections || [];
    const milestones = project.milestones || [];
    const todos = sections.flatMap((section) => (section.todoClusters || []).flatMap((cluster) => (cluster.todos || []).map((todo) => ({ ...todo, section, cluster }))));
    const total = (project.documents || []).length;
    const uploadTarget = () => {
      const [kind, id] = targetValue.split(':');
      return {
        planningSectionId: kind === 'feature' ? id : null,
        milestoneId: kind === 'milestone' ? id : null,
        todoId: kind === 'todo' ? id : null,
      };
    };
    const targetLabel = (doc) => {
      if (doc.todoId) {
        const todo = todos.find((item) => item.id === doc.todoId);
        return todo ? `Todo · ${todo.title}` : 'Todo';
      }
      if (doc.milestoneId) {
        const milestone = milestones.find((item) => item.id === doc.milestoneId);
        return milestone ? `Milestone · ${milestone.title}` : 'Milestone';
      }
      if (doc.planningSectionId) {
        const section = sections.find((item) => item.id === doc.planningSectionId);
        return section ? `Feature · ${section.name}` : 'Feature';
      }
      return 'Project base';
    };
    const uploadProject = (e) => {
      const files = e.target.files;
      e.target.value = '';
      uploadProjectDocumentFiles(project, onAction, uploadTarget(), files);
    };
    const docRow = (doc) => (
      <div key={doc.id} style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
        <span className="chip" style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}><Icon name="doc" size={12} />{doc.fileName}</span>
        <span className="chip" style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>{targetLabel(doc)}</span>
        <button className="icon-btn" title="Open document" onClick={() => onAction('openProjectDocument', { projectId: project.id, documentId: doc.id })}><Icon name="external" size={14} /></button>
        <button className="icon-btn" title="Remove document" onClick={() => onAction('deleteProjectDocument', { projectId: project.id, documentId: doc.id, fileName: doc.fileName })}><Icon name="trash" size={14} /></button>
      </div>
    );
    return (
      <section className="panel">
        <div className="panel-head pd-panelhead-btn" onClick={() => setOpen((o) => !o)}>
          <Icon name="chevR" size={14} className={'pd-chev' + (open ? ' open' : '')} style={{ color: 'var(--faint)', flex: 'none' }} />
          <div><h2>Documents</h2><div className="sub">{total} file{total !== 1 ? 's' : ''}</div></div>
          <div className="actions" onClick={(e) => e.stopPropagation()}>
            <select className="input sm" value={targetValue} onChange={(e) => setTargetValue(e.target.value)} style={{ width: 210 }}>
              <option value="project">Project base</option>
              {sections.map((section) => <option key={section.id} value={`feature:${section.id}`}>Feature · {section.name}</option>)}
              {milestones.map((milestone) => <option key={milestone.id} value={`milestone:${milestone.id}`}>Milestone · {milestone.title}</option>)}
              {todos.map((todo) => <option key={todo.id} value={`todo:${todo.id}`}>Todo · {todo.title}</option>)}
            </select>
            <label className="btn sm ghost" style={{ cursor: 'pointer' }}><Icon name="plus" size={13} />Upload<input type="file" multiple style={{ display: 'none' }} onChange={uploadProject} /></label>
          </div>
        </div>
        {open && (
          <div className="panel-body">
            <div style={{ display: 'grid', gap: 7 }}>{(project.documents || []).length ? project.documents.map(docRow) : <span className="muted" style={{ fontSize: 12 }}>No project documents.</span>}</div>
          </div>
        )}
      </section>
    );
  }

  /* ---------- Financials tab (tables identical to original ProjectDetail) ---------- */
  function FinancialsTab({ project, db, fin, onDrawer, onAction }) {
    return (
      <div className="grid">
        <div className="fin-summary">
          <KPI label="Total billed" icon="invoices" value={money(fin.billed)} delta={`${fin.invs.length} invoice${fin.invs.length !== 1 ? 's' : ''}`} />
          <KPI label="Outstanding" icon="wallet" tone={fin.outstanding > 0 ? 'warn' : 'pos'} value={fin.outstanding > 0 ? money(fin.outstanding) : '—'} delta={fin.outstanding > 0 ? 'Awaiting payment' : 'All settled'} />
          <KPI label="Deliverables & offers" icon="projects" value={`${project.projectDeliverables.length} · ${(project.offers || []).length}`} delta="Active scope" />
        </div>

        <Panel title="Deliverables & offers" sub={`${project.projectDeliverables.length} deliverable${project.projectDeliverables.length !== 1 ? 's' : ''} · ${(project.offers || []).length} offer${(project.offers || []).length !== 1 ? 's' : ''}`}
          actions={<div style={{ display: 'flex', gap: 6 }}><button className="btn sm ghost" onClick={() => onDrawer('projectOffer', null, { projectId: project.id })}><Icon name="doc" size={14} />Offer</button><button className="btn sm ghost" onClick={() => onDrawer('projectDeliverable', null, { projectId: project.id })}><Icon name="plus" size={14} />Deliverable</button></div>} flush>
          <DataTable
            rows={projectDeliverableRows(project, db)} rowKey={(row) => row.id} empty="No deliverables or offers yet."
            columns={[
              { header: 'Item', render: (row) => row.type === 'deliverable' ? <div className="cellstack"><span className="t-strong">{row.deliverable.name}</span><span className="t-sub">Deliverable</span></div> : <div className="cellstack"><span className="t-strong">{row.offer.title}</span><span className="t-sub">Offer · {(row.offer.lines || []).length} line{(row.offer.lines || []).length !== 1 ? 's' : ''}</span></div> },
              { header: 'Type', className: 'muted', render: (row) => row.type === 'deliverable' ? row.deliverable.kind : 'Offer' },
              { header: 'Status', render: (row) => row.type === 'deliverable' ? <Pill kind={row.deliverable.status === 'Closed' ? 'Inactive' : row.deliverable.status || 'Active'} sm /> : <Pill kind={row.offer.status || 'Offered'} sm /> },
              { header: 'Billing', render: (row) => row.type === 'deliverable' ? <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>{row.deliverable.freeOfCharge ? <span className="chip">Free of charge</span> : !row.milestoneBillable ? <span className="chip">Milestone open</span> : (row.deliverable.recurrenceFrequency && row.deliverable.recurrenceFrequency !== 'None') ? <span className="chip"><Icon name="refresh" size={11} />{row.deliverable.recurrenceFrequency}</span> : <span className="faint" style={{ whiteSpace: 'nowrap' }}>One-off</span>}{row.missingInvoiceCount > 0 && <span className="chip" style={{ background: 'var(--warn-soft)', color: 'var(--warn)', borderColor: '#f0d9b8' }} title="Invoices need to be created"><Icon name="invoices" size={11} />{row.missingInvoiceCount} due</span>}</div> : '—' },
              { header: 'Amount', className: 'num', render: (row) => row.type === 'deliverable' ? money(row.deliverable.unitPrice) : money(offerTotals(row.offer).total) },
              { header: 'Tax', className: 'num muted', render: (row) => row.type === 'deliverable' ? percent(row.deliverable.taxRate) : '—' },
            ]}
            actions={[
              { key: 'convert', label: 'Convert', show: (row) => row.type === 'offer' && row.offer.status !== 'Converted', onClick: (row) => onAction('convertProjectOffer', row.offer) },
              { key: 'download', title: 'Download PDF', icon: 'download', show: (row) => row.type === 'offer', onClick: (row) => onAction('downloadProjectOffer', row.offer) },
              { key: 'invoiceDeliverable', title: 'Create deliverable invoices', icon: 'invoices', show: (row) => row.type === 'deliverable' && !row.deliverable.freeOfCharge && row.milestoneBillable, onClick: (row) => onDrawer('deliverableInvoice', null, { projectId: project.id, deliverableId: row.deliverable.id }) },
              { key: 'editDeliverable', title: 'Edit deliverable', icon: 'edit', show: (row) => row.type === 'deliverable', onClick: (row) => onDrawer('projectDeliverable', row.deliverable.id, { projectId: project.id }) },
              { key: 'editOffer', title: 'Edit offer', icon: 'edit', show: (row) => row.type === 'offer' && row.offer.status !== 'Converted', onClick: (row) => onDrawer('projectOffer', row.offer.id, { projectId: project.id }) },
              { key: 'deleteDeliverable', title: 'Delete deliverable', icon: 'trash', show: (row) => row.type === 'deliverable', onClick: (row) => onAction('deleteProjectDeliverable', row.deliverable) },
              { key: 'deleteOffer', title: 'Delete offer', icon: 'trash', show: (row) => row.type === 'offer', onClick: (row) => onAction('deleteProjectOffer', row.offer) },
            ]} />
        </Panel>

        <Panel title="Invoices" sub={`${fin.invs.length} for this project`} flush>
          <DataTable
            rows={fin.invs} rowKey={(iv) => iv.id} empty="No invoices yet."
            columns={[
              { header: 'Invoice', className: 'mono t-strong', render: (iv) => iv.number },
              { header: 'Issue', className: 'mono muted', cellStyle: { fontSize: 11.5 }, render: (iv) => fmtDate(iv.issueDate) },
              { header: 'Status', render: (iv) => <Pill kind={MODEL.enrich(db, iv).display} sm /> },
              { header: 'Total', className: 'num', render: (iv) => money(MODEL.enrich(db, iv).totals.total) },
              { header: 'Outstanding', className: 'num', render: (iv) => { const e = MODEL.enrich(db, iv); return e.totals.outstanding > 0 ? money(e.totals.outstanding) : '—'; } },
            ]}
            actions={[
              { key: 'finalize', label: 'Finalize', show: (iv) => iv.status === 'Draft', onClick: (iv) => onAction('finalize', iv) },
              { key: 'pay', label: 'Mark paid', show: (iv) => MODEL.enrich(db, iv).open, onClick: (iv) => onAction('pay', iv) },
              { key: 'edit', title: 'Edit', icon: 'edit', show: (iv) => iv.status === 'Draft', onClick: (iv) => onDrawer('invoice', iv.id) },
              { key: 'delete', title: 'Delete invoice', icon: 'trash', show: (iv) => iv.status === 'Draft', onClick: (iv) => onAction('delete', iv) },
              { key: 'download', title: 'Download invoice', icon: 'download', onClick: (iv) => onAction('download', iv) },
              { key: 'open', title: 'Open invoice', icon: 'external', onClick: (iv) => onAction('print', iv) },
            ]} />
        </Panel>
      </div>
    );
  }

  /* ---------- Testing tab (hidden project test entities, not snapshot data) ---------- */
  const blankTest = () => ({
    name: '',
    status: 'Active',
    description: '',
    instructions: '',
    expectedResult: '',
    dataSetup: '',
    tags: '',
    lastRunNotes: '',
    todoId: '',
  });

  function tagText(tags) {
    return Array.isArray(tags) ? tags.join(', ') : (tags || '');
  }

  function ProjectTestingTab({ project }) {
    const [tests, setTests] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [selectedId, setSelectedId] = useState(null);
    const [draft, setDraft] = useState(blankTest);

    const selected = tests.find((test) => test.id === selectedId) || null;
    const todoOptions = (project.planningSections || [])
      .flatMap((section) => (section.todoClusters || []).flatMap((cluster) => (cluster.todos || []).map((todo) => ({ ...todo, section, cluster }))))
      .sort((a, b) => (a.number || 0) - (b.number || 0));
    const set = (key, value) => setDraft((current) => ({ ...current, [key]: value }));
    const load = React.useCallback(async () => {
      try {
        setLoading(true);
        const rows = await window.API.listProjectTests(project.id);
        setTests(rows || []);
        setError(null);
        if (selectedId && !(rows || []).some((test) => test.id === selectedId)) setSelectedId(null);
      } catch (e) {
        setError((e && e.message) || 'Failed to load project tests.');
      } finally {
        setLoading(false);
      }
    }, [project.id, selectedId]);

    React.useEffect(() => { load(); }, [load]);
    React.useEffect(() => {
      if (!selected) {
        setDraft(blankTest());
        return;
      }

      setDraft({
        name: selected.name || '',
        status: selected.status || 'Active',
        description: selected.description || '',
        instructions: selected.instructions || '',
        expectedResult: selected.expectedResult || '',
        dataSetup: selected.dataSetup || '',
        tags: tagText(selected.tags),
        lastRunNotes: selected.lastRunNotes || '',
        todoId: '',
      });
    }, [selected]);

    async function save(event) {
      event.preventDefault();
      try {
        const body = { ...draft, tags: draft.tags };
        const saved = selected ? await window.API.updateProjectTest(project.id, selected.id, body) : await window.API.createProjectTest(project.id, body);
        setSelectedId(saved.id);
        await load();
      } catch (e) {
        setError((e && e.message) || 'Failed to save project test.');
      }
    }

    async function remove(test) {
      if (!window.confirm(`Delete ${test.name}?`)) return;
      try {
        await window.API.deleteProjectTest(project.id, test.id);
        if (selectedId === test.id) setSelectedId(null);
        await load();
      } catch (e) {
        setError((e && e.message) || 'Failed to delete project test.');
      }
    }

    async function record(status) {
      if (!selected) return;
      try {
        await window.API.recordProjectTestRun(project.id, selected.id, { status, notes: draft.lastRunNotes, todoId: draft.todoId || null });
        await load();
      } catch (e) {
        setError((e && e.message) || 'Failed to record test run.');
      }
    }

    const rows = tests.map((test) => ({
      ...test,
      lastRunLabel: test.lastRunAt ? `${test.lastRunStatus || 'Recorded'} · ${fmtDate(String(test.lastRunAt).slice(0, 10))}` : 'Not run',
    }));
    const runRows = tests
      .flatMap((test) => (test.runs || []).map((run) => ({ ...run, testId: test.id, testName: test.name })))
      .sort((a, b) => String(b.occurredAt || '').localeCompare(String(a.occurredAt || '')));

    return (
      <div className="pd-test-layout">
        <div className="pd-test-stack">
          <Panel title="Test flows" sub={`${tests.length} hidden flow${tests.length !== 1 ? 's' : ''}`} actions={<button className="btn sm ghost" onClick={() => setSelectedId(null)}><Icon name="plus" size={14} />Test</button>} flush>
            {error && <div className="alert" style={{ margin: 12 }}>{error}</div>}
            {loading ? <div className="pd-test-empty">Loading tests...</div> : (
              <DataTable
                rows={rows}
                rowKey={(test) => test.id}
                empty="No test flows yet."
                onRowClick={(test) => setSelectedId(test.id)}
                getRowClassName={(test) => test.id === selectedId ? 'selected' : ''}
                columns={[
                  { header: 'Flow', render: (test) => <div className="cellstack"><span className="t-strong">{test.name}</span><span className="t-sub">{test.description || 'No summary'}</span></div> },
                  { header: 'Status', render: (test) => <Pill kind={test.status || 'Active'} sm /> },
                  { header: 'Last run', className: 'muted', render: (test) => test.lastRunLabel },
                  { header: 'Tags', render: (test) => <span className="pd-test-tags">{tagText(test.tags) || '—'}</span> },
                ]}
                actions={[
                  { key: 'edit', title: 'Edit test flow', icon: 'edit', onClick: (test) => setSelectedId(test.id) },
                  { key: 'delete', title: 'Delete test flow', icon: 'trash', onClick: remove },
                ]} />
            )}
          </Panel>

          <Panel title="Test runs" sub={`${runRows.length} recorded run${runRows.length !== 1 ? 's' : ''}`} flush>
            {loading ? <div className="pd-test-empty">Loading runs...</div> : (
              <DataTable
                rows={runRows}
                rowKey={(run) => run.id}
                empty="No test runs recorded."
                onRowClick={(run) => setSelectedId(run.testId)}
                columns={[
                  { header: 'When', className: 'mono muted', render: (run) => fmtDate(String(run.occurredAt).slice(0, 10)) },
                  { header: 'Flow', render: (run) => <span className="t-strong">{run.testName}</span> },
                  { header: 'Result', render: (run) => <Pill kind={run.status || 'Recorded'} sm /> },
                  { header: 'Todo', render: (run) => run.todoId ? <div className="cellstack"><span className="t-strong">{run.todoNumber ? `#${run.todoNumber} · ` : ''}{run.todoTitle || 'Linked todo'}</span><span className="t-sub">{run.resolved ? 'Resolved' : 'Open'}</span></div> : <span className="muted">—</span> },
                  { header: 'Resolution', render: (run) => run.todoId ? <Pill kind={run.resolved ? 'Completed' : 'Active'} sm>{run.resolved ? 'Resolved' : 'Open'}</Pill> : <span className="muted">—</span> },
                  { header: 'Notes', render: (run) => <span className="pd-test-run-notes">{run.notes || '—'}</span> },
                ]} />
            )}
          </Panel>
        </div>

        <Panel title={selected ? 'Edit test flow' : 'New test flow'} sub={selected ? 'Project-level testing entity' : 'Create a project-level testing entity'}>
          <form className="pd-test-form" onSubmit={save}>
            <div className="field"><label>Name</label><input className="inp" value={draft.name} onChange={(e) => set('name', e.target.value)} required /></div>
            <div className="pd-test-form-row">
              <div className="field"><label>Status</label><select className="inp" value={draft.status} onChange={(e) => set('status', e.target.value)}><option>Active</option><option>Draft</option><option>Archived</option></select></div>
              <div className="field"><label>Tags</label><input className="inp" value={draft.tags} onChange={(e) => set('tags', e.target.value)} placeholder="smoke, regression" /></div>
            </div>
            <div className="field"><label>Description</label><textarea className="inp" rows="3" value={draft.description} onChange={(e) => set('description', e.target.value)} /></div>
            <div className="field"><label>Instructions</label><textarea className="inp mono" rows="7" value={draft.instructions} onChange={(e) => set('instructions', e.target.value)} /></div>
            <div className="field"><label>Expected result</label><textarea className="inp" rows="4" value={draft.expectedResult} onChange={(e) => set('expectedResult', e.target.value)} /></div>
            <div className="field"><label>Data setup</label><textarea className="inp" rows="3" value={draft.dataSetup} onChange={(e) => set('dataSetup', e.target.value)} /></div>
            {selected && (
              <div className="pd-test-runbox">
                <div className="field"><label>Run notes</label><textarea className="inp" rows="3" value={draft.lastRunNotes} onChange={(e) => set('lastRunNotes', e.target.value)} /></div>
                <div className="field">
                  <label>Linked todo</label>
                  <select className="inp" value={draft.todoId} onChange={(e) => set('todoId', e.target.value)}>
                    <option value="">No todo link</option>
                    {todoOptions.map((todo) => <option key={todo.id} value={todo.id}>#{todo.number} · {todo.title}</option>)}
                  </select>
                </div>
                <div className="pd-test-run-actions">
                  <button type="button" className="btn sm ghost" onClick={() => record('Passed')}><Icon name="check" size={14} />Passed</button>
                  <button type="button" className="btn sm ghost" onClick={() => record('Failed')}><Icon name="alert" size={14} />Failed</button>
                  <button type="button" className="btn sm ghost" onClick={() => record('Blocked')}><Icon name="clock" size={14} />Blocked</button>
                </div>
              </div>
            )}
            <div className="pd-test-form-actions">
              {selected && <button type="button" className="btn ghost" onClick={() => setSelectedId(null)}>New flow</button>}
              <button className="btn" type="submit"><Icon name="check" size={14} />Save test</button>
            </div>
          </form>
        </Panel>
      </div>
    );
  }

  /* ---------- shell ---------- */
  function ProjectDetailView({ db, project, onBack, onDrawer, onAction, onToast, onError, onReload }) {
    const [tab, setTab] = useState('dashboard');
    const fin = projectFinance(db, project.id);
    const pr = progressOf(project);
    const planning = useProjectPlanning(project, onAction);
    const EmailComponent = window.EmailExplorer;
    const openMs = (project.milestones || []).filter((m) => !m.completed).length;
    const hasFinalizedInvoices = fin.invs.some((iv) => iv.status !== 'Draft' || iv.finalizedAt);

    // inline project field save → new onAction case (see README)
    const saveProject = (patch) => onAction('updateProject', { projectId: project.id, project, patch });
    async function deleteProject() { if (await onAction('deleteProject', project)) onBack(); }

    const statItems = [
      { k: 'Total billed', v: money(fin.billed) },
      { k: 'Outstanding', v: fin.outstanding > 0 ? money(fin.outstanding) : '—', tone: fin.outstanding > 0 ? 'warn' : '' },
      { k: 'Invoices', v: String(fin.invs.length) },
      { k: 'Progress', v: Math.round(pr.msPct * 100) + '%', tone: 'pos' },
    ];

    const tabs = [
      { id: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
      { id: 'planning', label: 'Planning', icon: 'projects', count: openMs },
      { id: 'board', label: 'Board', icon: 'dashboard', count: pr.todoTotal },
      { id: 'testing', label: 'Testing', icon: 'check' },
      { id: 'email', label: 'Email', icon: 'mail', count: (project.emails || []).length + (db.customers || []).flatMap((customer) => customer.correspondence || []).filter((item) => item.projectId === project.id).length },
      { id: 'financials', label: 'Financials', icon: 'invoices', count: fin.invs.length },
    ];

    return (
      <div className="pd-root">
        <div className="pd-topbar">
          <button className="btn subtle sm pd-back" onClick={onBack}><Icon name="chevL" size={14} />Projects</button>
          <span className="faint">/</span>
          <div className="pd-titlewrap">
            <div className="pd-crumb">{project.kind}</div>
            <h1><Inline value={project.name} onCommit={(v) => saveProject({ name: v })} /></h1>
          </div>
          <div style={{ flex: 1 }} />
          <div className="pd-actions">
            <button className="btn ghost sm" onClick={() => onDrawer('project', project.id)}><Icon name="edit" size={14} />Edit</button>
            <button className="btn ghost sm" onClick={() => onDrawer('projectOffer', null, { projectId: project.id })}><Icon name="doc" size={14} />Offer</button>
            <button className="btn ghost sm" disabled={hasFinalizedInvoices} title={hasFinalizedInvoices ? 'Project has finalized invoices' : 'Delete project'} onClick={deleteProject}><Icon name="trash" size={14} />Delete</button>
            <button className="btn sm" onClick={() => onDrawer('invoice', null, { projectId: project.id })}><Icon name="plus" size={14} />Invoice</button>
          </div>
        </div>

        <div className="pd-tabbar">
          {tabs.map((t) => (
            <button key={t.id} className={'pd-tab' + (tab === t.id ? ' on' : '')} onClick={() => setTab(t.id)}>
              <Icon name={t.icon} size={14} />{t.label}{t.count != null && <span className="pd-tabcount">{t.count}</span>}
            </button>
          ))}
        </div>

        <div className="pd-scroll">
          {tab === 'dashboard' && (
            <div>
              <Panel title="Project lifeline" sub="Milestones · billing · invoices across the project" style={{ marginBottom: 14 }}>
                <LifelineLanes project={project} db={db} />
              </Panel>
              <div className="grid" style={{ gridTemplateColumns: '1.05fr 1.4fr 1fr', alignItems: 'start' }}>
                <Panel title="Progress & health"><ProgressContent project={project} ringSize={104} /></Panel>
                <OverviewCard project={project} saveProject={saveProject} />
                <CustomersCard project={project} db={db} onDrawer={onDrawer} onAction={onAction} />
              </div>
            </div>
          )}
          {(tab === 'planning' || tab === 'board') && <PlanningSaveBar planning={planning} />}
          {tab === 'planning' && <ProjectPlanningExec planning={planning} project={project} db={db} onDrawer={onDrawer} onAction={onAction} DocumentsPanel={DocumentsPanel} Inline={Inline} />}
          {tab === 'board' && <ProjectBoardTab planning={planning} project={project} statuses={window.AC.PROJECT_TODO_STATUSES} onAction={onAction} />}
          {tab === 'testing' && <ProjectTestingTab project={project} />}
          {tab === 'email' && EmailComponent && <EmailComponent db={db} query="" onToast={onToast} onError={onError} onReload={onReload} onDrawer={onDrawer} onAction={onAction} scopeId={project.id} scopeProject={project} showProjectDrop={false} />}
          {tab === 'financials' && <FinancialsTab project={project} db={db} fin={fin} onDrawer={onDrawer} onAction={onAction} />}
        </div>
      </div>
    );
  }

  Object.assign(window, { ProjectDetailView, ProjectDocumentArea, projectDocumentsFor, projectDocumentDropProps, ProjectProgress: progressOf });
})();
