-- mockshot-seed.sql — reversible sample data at deployment scale, for
-- screenshots and demos (first used for the README operator-overview shot).
--
-- Identity: the database generates every ID (Decision 30), and this seed
-- addresses the rows it owns by `key` (grammar `^[a-z][a-z0-9_]*$`).
-- Organizations are keyed `mockshot_0001` .. `mockshot_2842` (one per mock
-- member) and `mockshot_t0001` .. `mockshot_t0038` (the teams). Each mock
-- person's user row carries the external reference `mockshot:<org key>`, so a
-- person is found by its OIDC subject and never by its email address, its
-- display name, or a marker column. The catalog rows are keyed
-- `mockshot_hosting` (entitlement set), `mockshot_starter_hosting` and its
-- four siblings (products), and `monthly` / `yearly` within each product
-- (prices). Everything else — pools, workspaces, assignments, accounts,
-- subscriptions, items, invoices, payments, grants, provisions — is reached
-- through its organization or its catalog parent, so it needs no key of its
-- own. mockshot-nuke.sql reverses the seed from exactly those three
-- predicates: the `mockshot_` organization key prefix, the `mockshot:`
-- subject prefix, and the catalog keys.
--
-- Re-running the seed inserts nothing: each keyed insert conflicts on its
-- key, and each keyless insert is guarded by a NOT EXISTS on the parent that
-- already carries the child.
--
-- Run against a test stack (port/creds from bootstrap-stack.sh output):
--   psql -h localhost -p $POSTGRES_PORT -U member_console -d member_console \
--     -v ON_ERROR_STOP=1 -f test/mockshot-seed.sql
-- Reverse with mockshot-nuke.sql the same way. Nuke BEFORE restarting the
-- app: the boot-time entitlement materializer must never reconcile the mock
-- provisions into real pool entitlements.
--
-- Shape follows the app's own mechanics: signup mints a personal org per
-- member (backdated over ~19 months of joins) with its default pool,
-- workspace, and default-plan grant; team orgs are created by long-standing
-- members; upgrading to a paid subscription ends the free default's delivery
-- (ladder exclusion); subscriptions carry priced items so monthly recurring
-- money is real.
--
-- Targets (existing → after): People 5→2,847 (~148 joined in 30 days),
-- Team orgs 0→38, Subscriptions 0→214 (205 active + 9 trialing, MRR
-- $2,160.00), Open invoices 0→9 ($372.00 outstanding), Delivering operator
-- grants →19 with ≈2,669 signup defaults in the caption, Catalog
-- products 10→15.
BEGIN;

-- 2,842 users, joined over ~19 months (i*5h + jitter). The subject carries
-- the organization key the member's personal org gets below, which is how
-- every later statement recovers the member's index without an ID of its own.
INSERT INTO core.users (oidc_subject, created_at)
SELECT 'mockshot:mockshot_' || lpad(i::text, 4, '0'),
       now() - (i * 5 || ' hours')::interval - (random() * interval '55 minutes')
FROM generate_series(1, 2842) AS i
ON CONFLICT (oidc_subject) DO NOTHING;

-- The member index, its organization key, and its join date, read back from
-- the rows themselves so a second run sees exactly what the first created.
-- `i` orders members by join date: member 1 is the newest, 2842 the oldest.
CREATE TEMP TABLE mock_members ON COMMIT DROP AS
SELECT split_part(u.oidc_subject, '_', 2)::int AS i,
       'mockshot_' || split_part(u.oidc_subject, '_', 2) AS org_key,
       u.user_id,
       u.created_at AS joined_at
FROM core.users u
WHERE u.oidc_subject LIKE 'mockshot:%';

INSERT INTO core.persons (user_id, display_name, primary_email, primary_email_verified, created_at)
SELECT
    m.user_id,
    -- Stride 7 is coprime to 20, so consecutive members vary in both names
    -- and (first, last) pairs stay unique across lcm(21,20)=420 indices.
    (ARRAY['Ada','Basil','Corin','Dara','Emil','Freya','Gus','Hana','Iris','Jonas','Kira',
           'Lior','Mara','Nils','Odile','Piotr','Quinn','Rosa','Sana','Theo','Uma'])[1 + (m.i % 21)]
    || ' ' ||
    (ARRAY['Aldana','Brook','Calder','Dove','Ellis','Fen','Grange','Hale','Ingram','Jarvis',
           'Keel','Lark','Marsh','North','Orr','Penn','Quill','Reyes','Sorrel','Vale'])[1 + ((m.i * 7) % 20)],
    -- Realistic data, never a predicate: nothing in this file or in the nuke
    -- selects on an address.
    'mock-p' || m.i || '@example.test',
    true,
    m.joined_at + interval '1 minute'
FROM mock_members m
WHERE NOT EXISTS (SELECT 1 FROM core.persons p WHERE p.user_id = m.user_id);

-- One personal org per mock person, keyed with the member's own key and timed
-- the way signup mints them (two minutes after the person row).
INSERT INTO core.organizations (name, key, org_type, owner_person_id, created_at)
SELECT p.display_name || '''s Organization',
       m.org_key,
       'personal',
       p.person_id,
       m.joined_at + interval '2 minutes'
FROM mock_members m
JOIN core.persons p ON p.user_id = m.user_id
ON CONFLICT (key) DO NOTHING;

-- The team org type (design taxonomy: personal / team / enterprise —
-- design/organization/architecture.md §2). Org types are vocabulary whose
-- primary key is the type itself, so there is nothing to key; the nuke
-- removes it only if no organization is left using it.
INSERT INTO core.org_types (org_type, display_name, description, is_active)
VALUES ('team', 'Team', 'An organization more than one person works in.', true)
ON CONFLICT (org_type) DO NOTHING;

-- 38 team orgs created by long-standing members (the oldest signups), each
-- 10-70 days after its owner joined.
INSERT INTO core.organizations (name, key, org_type, owner_person_id, created_at)
SELECT
    CASE WHEN k <= 4 THEN
        (ARRAY['Cedar Print Collective','Prairie Signal Cooperative',
               'Harbor Works Guild','Juniper Commons'])[k]
    ELSE
        (ARRAY['Cedar','Prairie','Harbor','Juniper','Granite','Meadow','Larkspur','Alder'])[1 + ((k - 5) % 8)]
        || ' ' ||
        (ARRAY['Collective','Cooperative','Commons','Works','Guild','Studio','Assembly','Exchange'])[1 + ((k - 5) / 8)]
    END,
    'mockshot_t' || lpad(k::text, 4, '0'),
    'team',
    p.person_id,
    m.joined_at + interval '10 days' + (random() * interval '60 days')
FROM generate_series(1, 38) AS k
JOIN mock_members m ON m.i = 2843 - k
JOIN core.persons p ON p.user_id = m.user_id
ON CONFLICT (key) DO NOTHING;

-- Every org gets the default pool and workspace signup mints for it
-- (provisioning.CreateWorkspaceWithPrimaryAssignment), so mock provisions
-- land in the org's own pool instead of borrowing somebody else's. The pool
-- is the one row the system keys by design, `default`; the workspace carries
-- no key, and the assignment is the pair, so neither does it.
INSERT INTO core.resource_pools (org_id, name, key, pool_type, is_auto_managed, created_at)
SELECT o.org_id, 'Default', 'default', 'default', TRUE, o.created_at
FROM core.organizations o
WHERE o.key LIKE 'mockshot\_%'
ON CONFLICT (org_id, key) DO NOTHING;

INSERT INTO core.workspaces (org_id, name, created_at)
SELECT o.org_id, 'Default', o.created_at
FROM core.organizations o
WHERE o.key LIKE 'mockshot\_%'
  AND NOT EXISTS (SELECT 1 FROM core.workspaces w WHERE w.org_id = o.org_id);

INSERT INTO core.pool_assignments (pool_id, workspace_id, is_primary, created_at)
SELECT rp.pool_id, w.workspace_id, TRUE, o.created_at
FROM core.organizations o
JOIN core.resource_pools rp ON rp.org_id = o.org_id AND rp.key = 'default'
JOIN core.workspaces w ON w.org_id = o.org_id
WHERE o.key LIKE 'mockshot\_%'
  AND NOT EXISTS (
      SELECT 1 FROM core.pool_assignments pa
      WHERE pa.pool_id = rp.pool_id AND pa.workspace_id = w.workspace_id
  );

-- Billing accounts: every team + the 176 most recent personal orgs.
INSERT INTO core.accounts (org_id, name, created_at)
SELECT o.org_id, o.name || ' billing', o.created_at
FROM (
    SELECT org_id, name, created_at
    FROM core.organizations
    WHERE key LIKE 'mockshot\_%'
    ORDER BY (key LIKE 'mockshot\_t%') DESC, created_at DESC
    LIMIT 214
) o
WHERE NOT EXISTS (SELECT 1 FROM core.accounts a WHERE a.org_id = o.org_id);

-- 214 live subscriptions: teams first (all active), then individuals;
-- the last 9 are trialing.
INSERT INTO core.subscriptions (billing_account_id, status, current_period_start, current_period_end)
SELECT billing_account_id,
       CASE WHEN rn <= 205 THEN 'active' ELSE 'trialing' END,
       date_trunc('month', now()),
       date_trunc('month', now()) + interval '1 month'
FROM (
    SELECT a.billing_account_id,
           row_number() OVER (ORDER BY (o.key LIKE 'mockshot\_t%') DESC, a.billing_account_id) AS rn
    FROM core.accounts a
    JOIN core.organizations o ON o.org_id = a.org_id
    WHERE o.key LIKE 'mockshot\_%'
      AND NOT EXISTS (SELECT 1 FROM core.subscriptions s WHERE s.billing_account_id = a.billing_account_id)
) accts;

-- The entitlement the mock plans confer. One set, one numeric rule, so the
-- mock provisions carry a real entitlement set instead of borrowing one.
INSERT INTO core.entitlement_sets (name, key, description, is_active)
VALUES ('Hosting baseline', 'mockshot_hosting',
        'Entitlements every hosting plan confers.', TRUE)
ON CONFLICT (key) DO NOTHING;

INSERT INTO core.entitlement_set_rules (set_id, rule_type, resource_key,
                                        resource_value, resource_per_unit, stacking_policy, description)
SELECT es.set_id, 'limit', 'fedwiki_sites', 5, FALSE, 'additive', 'Up to 5 sites.'
FROM core.entitlement_sets es
WHERE es.key = 'mockshot_hosting'
  AND NOT EXISTS (
      SELECT 1 FROM core.entitlement_set_rules r
      WHERE r.set_id = es.set_id AND r.resource_key = 'fedwiki_sites'
  );

-- Five mock products with deployment-plausible names (before the prices,
-- items, and grants that reference them). Published so grant events print
-- them.
INSERT INTO core.products (name, key, description, display_category,
                           entitlement_set_id, lifecycle_status)
SELECT v.name, v.key, v.description, v.display_category,
       CASE WHEN v.confers THEN es.set_id END,
       'published'
FROM (VALUES
    ('Starter Hosting',   'mockshot_starter_hosting',   'Default plan for new members',     NULL,    TRUE),
    ('Standard Hosting',  'mockshot_standard_hosting',  'Paid hosting tier',                NULL,    TRUE),
    ('Sustainer Hosting', 'mockshot_sustainer_hosting', 'Supporter hosting tier',           NULL,    TRUE),
    ('Custom Domain',     'mockshot_custom_domain',     'Serve a site on your own domain',  'addon', FALSE),
    ('Storage Boost',     'mockshot_storage_boost',     'Additional media storage',         'addon', FALSE)
) AS v(name, key, description, display_category, confers)
CROSS JOIN (SELECT set_id FROM core.entitlement_sets WHERE key = 'mockshot_hosting') es
ON CONFLICT (key) DO NOTHING;

-- Prices for the paid tiers, keyed within their product: Standard $9/mo
-- (default), Sustainer $18/mo (default) with an $180/yr alternative — the
-- yearly price exercises the one-twelfth normalization on the overview's
-- money headline.
INSERT INTO core.prices (product_id, key, currency, unit_amount, recurring_interval, is_default)
SELECT p.product_id, v.key, 'usd', v.unit_amount, v.recurring_interval, v.is_default
FROM (VALUES
    ('mockshot_standard_hosting',  'monthly',   900, 'month', TRUE),
    ('mockshot_sustainer_hosting', 'monthly',  1800, 'month', TRUE),
    ('mockshot_sustainer_hosting', 'yearly',  18000, 'year',  FALSE)
) AS v(product_key, key, unit_amount, recurring_interval, is_default)
JOIN core.products p ON p.key = v.product_key
ON CONFLICT (product_id, key) DO NOTHING;

-- Subscription items: teams ride Sustainer (every 4th on the yearly price),
-- individuals ride Standard monthly. The team's number comes out of its own
-- key, the only arithmetic the rest of the file needs.
INSERT INTO core.subscription_items (subscription_id, product_id, price_id, quantity)
SELECT s.subscription_id, pr.product_id, pr.price_id, 1
FROM core.subscriptions s
JOIN core.accounts a ON a.billing_account_id = s.billing_account_id
JOIN core.organizations o ON o.org_id = a.org_id
JOIN LATERAL (
    SELECT p.price_id, p.product_id
    FROM core.prices p
    JOIN core.products pd ON pd.product_id = p.product_id
    WHERE CASE
            WHEN o.key LIKE 'mockshot\_t%' THEN
                pd.key = 'mockshot_sustainer_hosting'
                AND p.key = CASE WHEN split_part(o.key, '_t', 2)::int % 4 = 0 THEN 'yearly' ELSE 'monthly' END
            ELSE pd.key = 'mockshot_standard_hosting' AND p.key = 'monthly'
          END
    LIMIT 1
) pr ON true
WHERE o.key LIKE 'mockshot\_%'
  AND NOT EXISTS (
      SELECT 1 FROM core.subscription_items si WHERE si.subscription_id = s.subscription_id
  );

-- The signup default-plan grant for every mock org, stamped minutes after
-- the org itself (system-authored: reason 'default' ⟺ no granting person,
-- per chk_grants_default_iff_system_authored).
INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id,
                         grant_reason, description, valid_from, created_at)
SELECT pd.product_id, o.org_id, NULL, 'default', 'Plan issued at signup',
       o.created_at + interval '2 minutes',
       o.created_at + interval '2 minutes'
FROM core.organizations o
CROSS JOIN (SELECT product_id FROM core.products WHERE key = 'mockshot_starter_hosting') pd
WHERE o.key LIKE 'mockshot\_%'
  AND NOT EXISTS (
      SELECT 1 FROM core.grants g
      WHERE g.granted_to_org_id = o.org_id AND g.grant_reason = 'default'
  );

-- Each default grant delivers in its own org's pool.
INSERT INTO core.pool_provisions (pool_id, grant_id, status, entitlement_set_id, product_id, activated_at)
SELECT rp.pool_id, g.grant_id, 'active', es.set_id, g.product_id, g.created_at
FROM core.grants g
JOIN core.organizations o ON o.org_id = g.granted_to_org_id AND o.key LIKE 'mockshot\_%'
JOIN core.resource_pools rp ON rp.org_id = o.org_id AND rp.key = 'default'
CROSS JOIN (SELECT set_id FROM core.entitlement_sets WHERE key = 'mockshot_hosting') es
WHERE g.grant_reason = 'default'
  AND NOT EXISTS (SELECT 1 FROM core.pool_provisions pp WHERE pp.grant_id = g.grant_id);

-- Auto-provisioning: every live mock subscription delivers its plan, so a
-- paying org is never provision-less after its free default ends below.
INSERT INTO core.pool_provisions (pool_id, subscription_id, status, entitlement_set_id, product_id)
SELECT rp.pool_id, s.subscription_id, 'active', es.set_id, si.product_id
FROM core.subscriptions s
JOIN core.accounts a ON a.billing_account_id = s.billing_account_id
JOIN core.organizations o ON o.org_id = a.org_id AND o.key LIKE 'mockshot\_%'
JOIN core.resource_pools rp ON rp.org_id = o.org_id AND rp.key = 'default'
JOIN core.subscription_items si ON si.subscription_id = s.subscription_id
CROSS JOIN (SELECT set_id FROM core.entitlement_sets WHERE key = 'mockshot_hosting') es
WHERE NOT EXISTS (
    SELECT 1 FROM core.pool_provisions pp
    WHERE pp.subscription_id = s.subscription_id AND pp.product_id = si.product_id
);

-- Eighteen operator-issued extras (upgrades, promos) over the last ~3 days,
-- on the 18 newest members' orgs. The operator is a real console person,
-- never one of the mock members: mock members are exactly the users whose
-- subject carries the mockshot prefix.
INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id,
                         grant_reason, description, valid_from, created_at)
SELECT pd.product_id,
       o.org_id,
       op.person_id,
       CASE WHEN m.i % 2 = 0 THEN 'manual' ELSE 'promotional' END,
       'Issued by an operator',
       now() - (m.i * interval '4 hours') - interval '20 minutes',
       now() - (m.i * interval '4 hours') - interval '20 minutes'
FROM mock_members m
JOIN core.organizations o ON o.key = m.org_key
JOIN core.products pd ON pd.key = CASE
        WHEN m.i % 4 = 1 THEN 'mockshot_standard_hosting'
        WHEN m.i % 4 = 2 THEN 'mockshot_sustainer_hosting'
        WHEN m.i % 4 = 3 THEN 'mockshot_custom_domain'
        ELSE                  'mockshot_storage_boost' END
CROSS JOIN LATERAL (
    SELECT p.person_id
    FROM core.persons p
    JOIN core.users u ON u.user_id = p.user_id
    WHERE u.oidc_subject NOT LIKE 'mockshot:%'
    ORDER BY (p.display_name ILIKE 'alice%') DESC, p.created_at
    LIMIT 1
) op
WHERE m.i <= 18
  AND NOT EXISTS (
      SELECT 1 FROM core.grants g
      WHERE g.granted_to_org_id = o.org_id AND g.grant_reason <> 'default'
  );

INSERT INTO core.pool_provisions (pool_id, grant_id, status, entitlement_set_id, product_id, activated_at)
SELECT rp.pool_id, g.grant_id, 'active', es.set_id, g.product_id, g.created_at
FROM core.grants g
JOIN core.organizations o ON o.org_id = g.granted_to_org_id AND o.key LIKE 'mockshot\_%'
JOIN core.resource_pools rp ON rp.org_id = o.org_id AND rp.key = 'default'
CROSS JOIN (SELECT set_id FROM core.entitlement_sets WHERE key = 'mockshot_hosting') es
WHERE g.grant_reason <> 'default'
  AND NOT EXISTS (SELECT 1 FROM core.pool_provisions pp WHERE pp.grant_id = g.grant_id);

-- Invoices, one per billing account across the first fifteen teams
-- (invoice-numbers change, data model v16.2): the number is the platform's
-- own, per billing account, a zero-padded four-digit decimal starting at
-- 0001 — and since every account here holds exactly one seeded invoice,
-- every one of the fifteen is numbered 0001. Each account's
-- next_invoice_number moves to 2 below, past the number this seed assigned.
--
-- 9 open invoices, on teams t0001..t0009, staggered over the last two days
-- for the timeline.
INSERT INTO core.invoices (billing_account_id, status, amount_due, amount_paid, currency,
                           period_start, period_end, due_date, created_at, invoice_number)
SELECT a.billing_account_id, 'open',
       (ARRAY[2400, 4800, 1200, 9600, 2400, 7200, 4800, 1200, 3600])[n], 0, 'usd',
       now() - interval '1 month', now(), now() + interval '14 days',
       now() - (n * interval '5 hours'),
       '0001'
FROM generate_series(1, 9) AS n
JOIN core.organizations o ON o.key = 'mockshot_t' || lpad(n::text, 4, '0')
JOIN core.accounts a ON a.org_id = o.org_id
WHERE NOT EXISTS (
    SELECT 1 FROM core.invoices i WHERE i.billing_account_id = a.billing_account_id
);

-- 6 recently paid invoices, on teams t0010..t0015, each settled by a
-- succeeded payment two minutes after issue — the timeline's
-- payment_received rows.
INSERT INTO core.invoices (billing_account_id, status, amount_due, amount_paid, currency,
                           period_start, period_end, due_date, paid_at, created_at, invoice_number)
SELECT a.billing_account_id, 'paid',
       (ARRAY[2400, 4800, 2400, 9600, 4800, 2400])[n],
       (ARRAY[2400, 4800, 2400, 9600, 4800, 2400])[n], 'usd',
       now() - interval '1 month', now(), now() + interval '14 days',
       now() - (n * interval '11 hours') + interval '2 minutes',
       now() - (n * interval '11 hours'),
       '0001'
FROM generate_series(1, 6) AS n
JOIN core.organizations o ON o.key = 'mockshot_t' || lpad((n + 9)::text, 4, '0')
JOIN core.accounts a ON a.org_id = o.org_id
WHERE NOT EXISTS (
    SELECT 1 FROM core.invoices i WHERE i.billing_account_id = a.billing_account_id
);

INSERT INTO core.payments (invoice_id, billing_account_id, amount, currency, status, created_at)
SELECT i.invoice_id, i.billing_account_id, i.amount_paid, 'usd', 'succeeded',
       i.created_at + interval '2 minutes'
FROM core.invoices i
JOIN core.accounts a ON a.billing_account_id = i.billing_account_id
JOIN core.organizations o ON o.org_id = a.org_id AND o.key LIKE 'mockshot\_t%'
WHERE i.status = 'paid'
  AND NOT EXISTS (SELECT 1 FROM core.payments pm WHERE pm.invoice_id = i.invoice_id);

-- Each of the fifteen accounts just numbered one invoice 0001; its counter
-- moves to 2. The `<> 2` guard makes a second run of this seed a no-op here
-- as well as everywhere above.
UPDATE core.accounts a
SET next_invoice_number = 2
FROM core.organizations o
WHERE o.org_id = a.org_id
  AND o.key LIKE 'mockshot\_t%'
  AND split_part(o.key, '_t', 2)::int <= 15
  AND a.next_invoice_number <> 2;

-- Upgrading ends the free default's delivery: the ladder table's exclusion
-- admits one active provision per pool per ladder, so each subscribed org's
-- default-grant provision ended when its paid subscription provisioned. The
-- grant itself stays 'active' in the ledger — exactly the ledger-vs-delivery
-- distinction the overview tile is built around.
UPDATE core.pool_provisions pp
SET status = 'ended',
    ended_at = pp.activated_at + interval '40 minutes'
FROM core.grants g
JOIN core.organizations o ON o.org_id = g.granted_to_org_id
JOIN core.accounts a ON a.org_id = g.granted_to_org_id
JOIN core.subscriptions s ON s.billing_account_id = a.billing_account_id
WHERE pp.grant_id = g.grant_id
  AND g.grant_reason = 'default'
  AND o.key LIKE 'mockshot\_%'
  AND pp.status <> 'ended';

COMMIT;

-- Post-seed tallies
SELECT (SELECT count(*) FROM core.persons WHERE status = 'active')                                    AS people,
       (SELECT count(*) FROM core.persons WHERE status = 'active'
          AND created_at > now() - interval '30 days')                                                AS joined_30d,
       (SELECT count(*) FROM core.organizations o JOIN core.org_types t ON t.org_type = o.org_type
          WHERE o.status = 'active' AND o.org_type <> 'personal' AND NOT t.is_reserved)               AS team_orgs,
       (SELECT count(*) FROM core.subscriptions WHERE status = 'active')                              AS active_subs,
       (SELECT count(*) FROM core.subscriptions WHERE status = 'trialing')                            AS trialing_subs,
       (SELECT sum(CASE p.recurring_interval WHEN 'month' THEN p.unit_amount * si.quantity
                        WHEN 'year' THEN (p.unit_amount * si.quantity) / 12 ELSE 0 END)
          FROM core.subscriptions s
          JOIN core.subscription_items si ON si.subscription_id = s.subscription_id
          JOIN core.prices p ON p.price_id = si.price_id
          WHERE s.status = 'active')                                                                  AS mrr_cents,
       (SELECT count(*) FROM core.invoices WHERE status = 'open')                                     AS open_invoices,
       (SELECT count(DISTINCT g.grant_id) FROM core.grants g
          JOIN core.pool_provisions pp ON pp.grant_id = g.grant_id
          WHERE pp.status = 'active' AND g.grant_reason <> 'default')                                 AS operator_grants,
       (SELECT count(DISTINCT g.grant_id) FROM core.grants g
          JOIN core.pool_provisions pp ON pp.grant_id = g.grant_id
          WHERE pp.status = 'active' AND g.grant_reason = 'default')                                  AS default_grants;
