#!/usr/bin/env node

const baseUrl = process.argv[2] ?? process.env.API_BASE_URL ?? "http://127.0.0.1:8080";
const email = `admin+${Date.now()}@example.com`;

async function request(method, path, body, token, options = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    method,
    headers: {
      "Content-Type": "application/json",
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const text = await response.text();
  const data = text ? parseResponseBody(text) : {};
  const expectedStatus = options.expectedStatus;

  if (expectedStatus !== undefined) {
    if (response.status !== expectedStatus) {
      throw new Error(`${method} ${path} expected ${expectedStatus}, got ${response.status}: ${JSON.stringify(data)}`);
    }
    return data;
  }

  if (!response.ok) {
    throw new Error(`${method} ${path} failed: ${response.status} ${JSON.stringify(data)}`);
  }

  return data;
}

function parseResponseBody(text) {
  try {
    return JSON.parse(text);
  } catch {
    return { message: text };
  }
}

function assert(condition, message) {
  if (!condition) throw new Error(message);
}

async function main() {
  console.log(`testing onboarding api via ${baseUrl}`);

  const registration = await request("POST", "/api/v1/registration/organization", {
    organization_name: "Muster GmbH",
    email,
    accept_terms: true,
  });
  assert(registration.id, "registration id missing");
  console.log(`registered ${registration.id}`);

  const registrations = await request("GET", "/api/v1/admin/organization-registrations");
  assert(
    registrations.some((item) => item.id === registration.id),
    "registration not found in list",
  );

  const detail = await request(
    "GET",
    `/api/v1/admin/organization-registrations/${registration.id}`,
  );
  assert(detail.organization_name === "Muster GmbH", "organization name did not decrypt");
  assert(detail.email === email, "registration email mismatch");

  const approval = await request(
    "POST",
    `/api/v1/admin/organization-registrations/${registration.id}/approve`,
  );
  assert(approval.organization_id, "organization id missing");
  assert(approval.schema_name?.startsWith("company_"), "schema name missing");
  assert(approval.dev_initial_password, "dev initial password missing");
  console.log(`approved ${approval.organization_id}`);

  const login = await request("POST", "/api/v1/auth/login", {
    email,
    password: approval.dev_initial_password,
  });
  assert(login.must_change_password === true, "must_change_password should be true");
  assert(login.access_token, "access token missing");
  assert(login.organization_id, "selected organization missing");
  assert(login.organizations.length >= 1, "login organizations missing");
  const token = login.access_token;

  const selected = await request(
    "POST",
    "/api/v1/auth/select-organization",
    { organization_id: login.organization_id },
    token,
  );
  assert(selected.selected === true, "organization was not selected");

  const users = await request("GET", "/api/v1/organizations/current/users", undefined, token);
  assert(users.length >= 1, "users list is empty");
  assert(users.some((user) => user.email === email), "owner user missing");

  const setup = await request("PUT", "/api/v1/organizations/current/setup", {
    display_name: "Muster GmbH",
    legal_form: "GmbH",
    street: "Musterstrasse 1",
    postal_code: "12345",
    city: "Musterstadt",
    country: "Deutschland",
    vat_id: "",
    email: "info@example.com",
    phone: "",
    default_tax_rate: "19",
    default_payment_days: "14",
  }, token);
  assert(setup.saved === true, "organization setup was not saved");

  const loadedSetup = await request("GET", "/api/v1/organizations/current/setup", undefined, token);
  assert(loadedSetup.setup?.display_name === "Muster GmbH", "organization setup was not loaded");
  assert(loadedSetup.setup?.street === "Musterstrasse 1", "organization setup street mismatch");

  const numberRanges = await request("GET", "/api/v1/number-ranges", undefined, token);
  assert(numberRanges.some((range) => range.code === "customers" && range.pattern === "KU{counter}"), "customer number range missing");
  assert(numberRanges.some((range) => range.code === "items" && range.pattern === "AR{counter}"), "item number range missing");
  assert(numberRanges.some((range) => range.code === "activities" && range.pattern === "AK{counter}"), "activity number range missing");
  assert(numberRanges.some((range) => range.code === "outgoing_invoices" && range.pattern === "AR{counter}"), "invoice number range missing");

  const cashDiscountTerm = await request("POST", "/api/v1/cash-discount-terms", {
    code: "2-10-30",
    name: "2 % Skonto bei Zahlung innerhalb von 10 Tagen",
    discount_percent: "2.00",
    discount_days: 10,
    net_days: 30,
    valid_from: null,
    valid_until: null,
    is_default_customer_term: true,
    is_default_supplier_term: true,
    is_active: true,
  }, token);
  assert(cashDiscountTerm.id, "cash discount term id missing");
  const cashDiscountTerms = await request("GET", "/api/v1/cash-discount-terms", undefined, token);
  assert(cashDiscountTerms.some((term) => term.id === cashDiscountTerm.id), "cash discount term missing");

  const createdCustomer = await request("POST", "/api/v1/customers", {
    customer_number: "",
    name: "Beispielkunde GmbH",
    status: "active",
    details: {
      street: "Kundenstraße 4",
      postal_code: "54321",
      city: "Kundenstadt",
      country: "Deutschland",
      email: "kunde@example.com",
      phone: "01234 56789",
    },
    standard_discount_percent: "5.50",
    cash_discount_term_id: cashDiscountTerm.id,
  }, token);
  assert(createdCustomer.id, "customer id missing");
  assert(/^KU\d{3}\.\d{3}\.\d{3}$/.test(createdCustomer.customer_number), "customer number was not generated");

  const customers = await request("GET", "/api/v1/customers", undefined, token);
  const listedCustomer = customers.find((customer) => customer.id === createdCustomer.id);
  assert(listedCustomer?.name === "Beispielkunde GmbH", "customer name was not loaded");
  assert(listedCustomer?.details.city === "Kundenstadt", "customer details were not loaded");
  assert(listedCustomer?.standard_discount_percent.startsWith("5.5"), "customer discount missing");
  assert(listedCustomer?.cash_discount_term_id === cashDiscountTerm.id, "customer cash discount missing");

  const updatedCustomer = await request("PUT", `/api/v1/customers/${createdCustomer.id}`, {
    ...createdCustomer,
    name: "Beispielkunde AG",
    standard_discount_percent: "7.00",
  }, token);
  assert(updatedCustomer.name === "Beispielkunde AG", "customer update failed");

  const supplier = await request("POST", "/api/v1/suppliers", {
    supplier_number: "",
    name: "Beispiellieferant GmbH",
    status: "active",
    details: { street: "Lieferweg 1", postal_code: "10115", city: "Berlin", country: "Deutschland", email: "lieferant@example.com", phone: "" },
    standard_discount_percent: "2.00",
    cash_discount_term_id: cashDiscountTerm.id,
    payment_days: 30,
  }, token);
  assert(/^LI\d{3}\.\d{3}\.\d{3}$/.test(supplier.supplier_number), "supplier number was not generated");
  const suppliers = await request("GET", "/api/v1/suppliers", undefined, token);
  assert(suppliers.some((record) => record.id === supplier.id && record.details.city === "Berlin"), "supplier CRUD failed");
  assert(suppliers.some((record) => record.id === supplier.id && record.cash_discount_term_id === cashDiscountTerm.id), "supplier cash discount missing");

  const item = await request("POST", "/api/v1/items", {
    item_number: "",
    name: "Montagestunde",
    unit: "Std",
    tax_rate: "19",
    default_purchase_price: "40.00",
    default_sales_price: "85.00",
    status: "active",
  }, token);
  assert(/^AR\d{3}\.\d{3}\.\d{3}$/.test(item.item_number), "item number was not generated");
  const updatedItem = await request("PUT", `/api/v1/items/${item.id}`, { ...item, default_sales_price: "95.00" }, token);
  assert(updatedItem.default_sales_price === "95.00", "item update failed");
  const priceHistory = await request("GET", `/api/v1/items/${item.id}/prices`, undefined, token);
  assert(priceHistory.length >= 2, "item price history missing");
  assert(priceHistory.some((entry) => entry.sales_price?.startsWith("95")), "updated item price history missing");

  const priceListContent = [
    "item_number;name;unit;tax_rate;purchase_price;sales_price",
    "IMP-100;Importartikel;Stk;19;10.00;25.00",
    `${item.item_number};Montagestunde Import;Std;19;42.00;99.00`,
  ].join("\n");
  const importPreview = await request("POST", "/api/v1/imports/price-list/preview", {
    source_name: "api-test-price-list.csv",
    delimiter: ";",
    content: priceListContent,
  }, token);
  assert(importPreview.total_rows === 2, "price import preview row count mismatch");
  assert(importPreview.valid_rows === 2, "price import preview valid rows mismatch");
  assert(importPreview.rows.some((row) => row.item_number === "IMP-100" && row.action === "create"), "price import create action missing");
  assert(importPreview.rows.some((row) => row.item_number === item.item_number && row.action === "update"), "price import update action missing");

  const importApply = await request("POST", "/api/v1/imports/price-list/apply", {
    source_name: "api-test-price-list.csv",
    delimiter: ";",
    content: priceListContent,
  }, token);
  assert(importApply.import_id, "price import id missing");
  assert(importApply.applied_rows === 2, "price import applied rows mismatch");
  assert(importApply.error_rows === 0, "price import errors mismatch");

  const importedItems = await request("GET", "/api/v1/items", undefined, token);
  assert(importedItems.some((record) => record.item_number === "IMP-100" && record.name === "Importartikel"), "imported item missing");
  const reloadedItem = importedItems.find((record) => record.id === item.id);
  assert(reloadedItem?.default_sales_price?.startsWith("99"), "imported item price update missing");
  const importedPriceHistory = await request("GET", `/api/v1/items/${item.id}/prices`, undefined, token);
  assert(importedPriceHistory.some((entry) => entry.source.startsWith("import:") && entry.sales_price?.startsWith("99")), "import price history missing");

  const connector = await request("POST", "/api/v1/api-connectors", {
    code: "demo_price_api",
    name: "Demo Preis API",
    connector_type: "demo",
    config: {
      base_url: "https://example.test/api",
      token: "secret",
      delimiter: ";",
      price_list_csv: [
        "item_number;name;unit;tax_rate;purchase_price;sales_price",
        "IMP-100;Importartikel API;Stk;19;11.00;27.00",
      ].join("\n"),
    },
    is_active: true,
    sync_interval_minutes: 60,
  }, token);
  assert(connector.id, "api connector id missing");
  assert(connector.config?.token === "secret", "api connector config roundtrip failed");
  const connectors = await request("GET", "/api/v1/api-connectors", undefined, token);
  assert(connectors.some((record) => record.id === connector.id && record.config?.base_url === "https://example.test/api"), "api connector list missing");
  const connectorSync = await request("POST", `/api/v1/api-connectors/${connector.id}/sync`, {}, token);
  assert(connectorSync.synced === true, "api connector sync failed");
  assert(connectorSync.applied_rows === 1, "api connector price sync did not apply rows");
  const syncedItems = await request("GET", "/api/v1/items", undefined, token);
  assert(syncedItems.some((record) => record.item_number === "IMP-100" && record.default_sales_price?.startsWith("27")), "api connector price update missing");
  const deletedConnector = await request("DELETE", `/api/v1/api-connectors/${connector.id}`, undefined, token);
  assert(deletedConnector.deleted === true, "api connector deactivate failed");

  const priceRule = await request("POST", "/api/v1/price-rules", {
    code: "standard_import_markup",
    name: "Standardaufschlag Import",
    source_type: "import",
    source_id: null,
    markup_percent: "25.00",
    rounding_mode: "cent",
    is_active: true,
  }, token);
  assert(priceRule.id, "price rule id missing");
  const priceRules = await request("GET", "/api/v1/price-rules", undefined, token);
  assert(priceRules.some((record) => record.id === priceRule.id && record.markup_percent.startsWith("25")), "price rule list missing");
  const updatedPriceRule = await request("PUT", `/api/v1/price-rules/${priceRule.id}`, {
    ...priceRule,
    markup_percent: "30.00",
    rounding_mode: "five_cent",
  }, token);
  assert(updatedPriceRule.rounding_mode === "five_cent", "price rule update failed");
  const deletedPriceRule = await request("DELETE", `/api/v1/price-rules/${priceRule.id}`, undefined, token);
  assert(deletedPriceRule.deleted === true, "price rule deactivate failed");

  const quote = await request("POST", "/api/v1/quotes", {
    quote_number: "",
    customer_id: createdCustomer.id,
    status: "draft",
    valid_until: null,
    cash_discount_term_id: cashDiscountTerm.id,
    customer_discount_percent: "7.00",
    notes: "Erstes Testangebot.",
    items: [{
      item_id: item.id,
      description: "Montagestunde mit Sonderpreis",
      quantity: "2.00",
      unit_price: "90.00",
      original_unit_price: "95.00",
      discount_percent: "0.00",
      tax_rate: "19.00",
    }],
  }, token);
  assert(/^AN\d{3}\.\d{3}\.\d{3}$/.test(quote.quote_number), "quote number was not generated");
  assert(quote.items.length === 1, "quote item missing");
  assert(quote.items[0].price_overridden === true, "quote price override missing");

  const quotes = await request("GET", "/api/v1/quotes", undefined, token);
  assert(quotes.some((record) => record.id === quote.id && record.notes.includes("Testangebot")), "quote was not loaded");
  const updatedQuote = await request("PUT", `/api/v1/quotes/${quote.id}`, {
    ...quote,
    status: "sent",
    items: quote.items.map((line) => ({ ...line, quantity: "3.00" })),
  }, token);
  assert(updatedQuote.status === "sent", "quote update failed");
  const convertedInvoice = await request("POST", `/api/v1/quotes/${quote.id}/convert-to-invoice`, undefined, token);
  assert(/^AR\d{3}\.\d{3}\.\d{3}$/.test(convertedInvoice.invoice_number), "converted invoice number missing");
  assert(convertedInvoice.source_quote_id === quote.id, "converted invoice quote link missing");
  assert(convertedInvoice.items.length === 1, "converted invoice item missing");
  const invoices = await request("GET", "/api/v1/outgoing-invoices", undefined, token);
  assert(invoices.some((record) => record.id === convertedInvoice.id), "outgoing invoice was not loaded");
  const finalized = await request("POST", `/api/v1/outgoing-invoices/${convertedInvoice.id}/finalize`, undefined, token);
  assert(finalized.finalized === true, "outgoing invoice finalize failed");
  const deletedQuote = await request("DELETE", `/api/v1/quotes/${quote.id}`, undefined, token);
  assert(deletedQuote.deleted === true, "quote cancel failed");

  const incomingInvoice = await request("POST", "/api/v1/incoming-invoices", {
    invoice_number: "EXT-10001",
    supplier_id: supplier.id,
    status: "received",
    cash_discount_term_id: cashDiscountTerm.id,
    invoice_date: null,
    due_at: null,
    items: [{
      item_id: item.id,
      description: "Einkauf Montagestunde",
      quantity: "1.00",
      unit_price: "40.00",
      tax_rate: "19.00",
    }],
  }, token);
  assert(incomingInvoice.id, "incoming invoice id missing");
  assert(incomingInvoice.cash_discount_term_id === cashDiscountTerm.id, "incoming invoice cash discount missing");
  const incomingInvoices = await request("GET", "/api/v1/incoming-invoices", undefined, token);
  assert(incomingInvoices.some((record) => record.id === incomingInvoice.id), "incoming invoice was not loaded");
  const deletedIncomingInvoice = await request("DELETE", `/api/v1/incoming-invoices/${incomingInvoice.id}`, undefined, token);
  assert(deletedIncomingInvoice.deleted === true, "incoming invoice cancel failed");

  const deletedItem = await request("DELETE", `/api/v1/items/${item.id}`, undefined, token);
  assert(deletedItem.deleted === true, "item deactivate failed");

  const deletedSupplier = await request("DELETE", `/api/v1/suppliers/${supplier.id}`, undefined, token);
  assert(deletedSupplier.deleted === true, "supplier deactivate failed");

  const deletedCustomer = await request("DELETE", `/api/v1/customers/${createdCustomer.id}`, undefined, token);
  assert(deletedCustomer.deleted === true, "customer deactivate failed");

  const activity = await request("POST", "/api/v1/activities", {
    activity_number: null,
    activity_type: "task",
    title: "Angebot prüfen",
    body: "Preise mit Kunde abstimmen.",
    status: "open",
    priority: "high",
    due_at: null,
  }, token);
  assert(/^AK\d{3}\.\d{3}\.\d{3}$/.test(activity.activity_number), "activity number was not generated");
  const activities = await request("GET", "/api/v1/activities", undefined, token);
  assert(activities.some((record) => record.id === activity.id && record.body.includes("Preise")), "activity CRUD failed");
  const deletedActivity = await request("DELETE", `/api/v1/activities/${activity.id}`, undefined, token);
  assert(deletedActivity.deleted === true, "activity cancel failed");

  const communication = await request("POST", "/api/v1/communications", {
    communication_type: "email",
    direction: "outbound",
    subject: "Rückfrage zum Angebot",
    body: "Kunde bittet um aktualisierte Dokumente.",
    status: "open",
    occurred_at: null,
    links: [{ entity_type: "customer", entity_id: createdCustomer.id }],
  }, token);
  assert(communication.id, "communication id missing");
  assert(communication.subject === "Rückfrage zum Angebot", "communication subject mismatch");
  assert(communication.links.some((link) => link.entity_id === createdCustomer.id), "communication link missing");
  const communications = await request("GET", "/api/v1/communications", undefined, token);
  assert(communications.some((record) => record.id === communication.id), "communication list missing");

  const documentContent = Buffer.from("Dokumentinhalt für Phase 6", "utf8").toString("base64");
  const documentRecord = await request("POST", "/api/v1/documents", {
    title: "Testdokument",
    description: "Dokument für Phase 6",
    file_name: "phase-6.txt",
    content_type: "text/plain",
    content_base64: documentContent,
    links: [{ entity_type: "communication", entity_id: communication.id }],
  }, token);
  assert(documentRecord.id, "document id missing");
  assert(documentRecord.latest_version?.file_name === "phase-6.txt", "document metadata missing");
  const documents = await request("GET", "/api/v1/documents", undefined, token);
  assert(documents.some((record) => record.id === documentRecord.id), "document list missing");
  const downloadedDocument = await request("GET", `/api/v1/documents/${documentRecord.id}/download`, undefined, token);
  assert(downloadedDocument.content_base64 === documentContent, "document download content mismatch");
  const auditLog = await request("GET", `/api/v1/documents/${documentRecord.id}/audit-log`, undefined, token);
  assert(auditLog.some((entry) => entry.action === "upload"), "document upload audit missing");
  assert(auditLog.some((entry) => entry.action === "download"), "document download audit missing");
  const deletedDocument = await request("DELETE", `/api/v1/documents/${documentRecord.id}`, undefined, token);
  assert(deletedDocument.deleted === true, "document archive failed");

  const invitedEmail = `user+${Date.now()}@example.com`;
  const invitation = await request("POST", "/api/v1/organizations/current/invitations", {
    email: invitedEmail,
    roles: ["viewer"],
  }, token);
  assert(invitation.id, "invitation id missing");
  assert(invitation.dev_invitation_token, "dev invitation token missing");

  const invitedUsers = await request("GET", "/api/v1/organizations/current/users", undefined, token);
  const invitedUser = invitedUsers.find((user) => user.user_id === invitation.user_id);
  assert(invitedUser, "invited user missing");
  const acceptedInvitation = await request("POST", "/api/v1/auth/accept-invitation", {
    token: invitation.dev_invitation_token,
    new_password: "InvitePass123",
    new_password_confirm: "InvitePass123",
  });
  assert(acceptedInvitation.accepted === true, "invitation accept failed");
  const invitedLogin = await request("POST", "/api/v1/auth/login", {
    email: invitedEmail,
    password: "InvitePass123",
  });
  assert(invitedLogin.access_token, "invited user login failed");
  assert(invitedLogin.organization_id, "invited user selected organization missing");
  const invitedToken = invitedLogin.access_token;

  await request(
    "POST",
    "/api/v1/auth/select-organization",
    { organization_id: invitedLogin.organization_id },
    invitedToken,
  );

  const deniedCustomerWrite = await request("POST", "/api/v1/customers", {
    customer_number: "",
    name: "Nicht erlaubt GmbH",
    status: "active",
    details: {
      street: "Sperrweg 1",
      postal_code: "12345",
      city: "Teststadt",
      country: "Deutschland",
      email: "nicht-erlaubt@example.test",
      phone: "",
    },
    standard_discount_percent: "0",
    cash_discount_term_id: null,
  }, invitedToken, { expectedStatus: 403 });
  assert(deniedCustomerWrite.message === "Berechtigung fehlt", "viewer customer write was not forbidden");

  const deniedRoleWrite = await request(
    "PATCH",
    `/api/v1/organizations/current/users/${invitation.user_id}/roles`,
    { roles: ["admin"] },
    invitedToken,
    { expectedStatus: 403 },
  );
  assert(deniedRoleWrite.message === "Berechtigung fehlt", "viewer role write was not forbidden");

  await request(
    "PATCH",
    `/api/v1/organizations/current/users/${invitation.user_id}/roles`,
    { roles: ["sales", "viewer"] },
    token,
  );
  const updatedUsers = await request("GET", "/api/v1/organizations/current/users", undefined, token);
  const updatedUser = updatedUsers.find((user) => user.user_id === invitation.user_id);
  assert(updatedUser.roles.includes("sales"), "role change was not saved");

  const resetRequest = await request("POST", "/api/v1/auth/request-password-reset", { email });
  assert(resetRequest.queued === true, "password reset request failed");
  assert(resetRequest.dev_reset_token, "dev reset token missing");
  const resetPassword = await request("POST", "/api/v1/auth/reset-password", {
    token: resetRequest.dev_reset_token,
    new_password: "ResetPass123",
    new_password_confirm: "ResetPass123",
  });
  assert(resetPassword.changed === true, "password reset failed");
  const resetLogin = await request("POST", "/api/v1/auth/login", {
    email,
    password: "ResetPass123",
  });
  assert(resetLogin.access_token, "login after password reset failed");

  console.log("onboarding api test ok");
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
