/**
 * Dual-write fail tests for withdrawals + refunds (Mongo-fail and MySQL-fail).
 */
import fs from "fs";
import path from "path";
import mongoose from "mongoose";
import mysql from "mysql2/promise";
import { connectMysql, disconnectMysql } from "../src/db/mysql/pool";
import { withFinancialDualWrite } from "../src/db/dualWriteFinancial";
import {
  upsertWithdrawalMysql,
  deleteWithdrawalMysql,
  insertRefundMysql,
  deleteRefundMysql,
} from "../src/db/mysql/financialWrites";
import { createId } from "../src/db/ids";
import { WithdrawalRequest, Refund } from "../src/db/models";

function loadEnv() {
  const t = fs.readFileSync(path.join(process.cwd(), ".env"), "utf8");
  for (const line of t.split(/\r?\n/)) {
    const s = line.trim();
    if (!s || s.startsWith("#")) continue;
    const eq = s.indexOf("=");
    if (eq <= 0) continue;
    const k = s.slice(0, eq).trim();
    let v = s.slice(eq + 1).trim();
    if (
      (v.startsWith('"') && v.endsWith('"')) ||
      (v.startsWith("'") && v.endsWith("'"))
    )
      v = v.slice(1, -1);
    process.env[k] = v;
  }
}

async function main() {
  loadEnv();
  process.env.DUAL_WRITE = "true";
  process.env.ACTIVE_DATABASE = "mongodb";
  delete process.env.DUAL_WRITE_FINANCIAL_FAIL;
  delete process.env.DUAL_WRITE_MONGO_FAIL;

  await mongoose.connect(process.env.MONGODB_URI!);
  await connectMysql();
  const pool = await mysql.createPool({
    host: process.env.MYSQL_HOST,
    port: Number(process.env.MYSQL_PORT),
    user: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD,
    database: process.env.MYSQL_DATABASE,
  });

  const results: { name: string; pass: boolean; detail: string }[] = [];
  const check = (name: string, pass: boolean, detail: string) => {
    results.push({ name, pass, detail });
    console.log(`${pass ? "PASS" : "FAIL"}  ${name} — ${detail}`);
  };

  const owner = await mongoose.connection.db!.collection("users").findOne({
    role: "OWNER",
    deletedAt: null,
  });
  if (!owner) throw new Error("no owner");
  const ownerId = String(owner._id);

  // ---- MySQL fail: withdrawal create compensates Mongo ----
  process.env.DUAL_WRITE_FINANCIAL_FAIL = "1";
  delete process.env.DUAL_WRITE_MONGO_FAIL;
  const createIdW = createId();
  let createMysqlFailThrew = false;
  try {
    await withFinancialDualWrite({
      site: "withdrawal.create",
      mongoWrite: async () =>
        WithdrawalRequest.create({
          _id: createIdW,
          ownerId,
          amountTnd: 10,
          amountLyd: 14,
          exchangeRateRate: 1.4,
          method: "BANK_LYD",
          status: "PENDING",
          note: "fail-mysql-create",
        }),
      mysqlWrite: async (doc) => {
        await upsertWithdrawalMysql({
          id: String(doc._id),
          ownerId,
          amountTnd: 10,
          amountLyd: 14,
          exchangeRateRate: 1.4,
          method: "BANK_LYD",
          status: "PENDING",
          note: "fail-mysql-create",
        });
      },
      mongoCompensate: async (doc) => {
        await WithdrawalRequest.deleteOne({ _id: doc._id });
      },
    });
  } catch {
    createMysqlFailThrew = true;
  }
  const mongoCreateLeft = await WithdrawalRequest.findById(createIdW).lean();
  const [mysqlCreateLeft] = await pool.query(
    `SELECT id FROM withdrawal_requests WHERE id = ?`,
    [createIdW],
  );
  check("withdrawal_create_mysql_fail_throws", createMysqlFailThrew, `threw=${createMysqlFailThrew}`);
  check(
    "withdrawal_create_mysql_fail_mongo_compensated",
    !mongoCreateLeft,
    `mongoLeft=${!!mongoCreateLeft}`,
  );
  check(
    "withdrawal_create_mysql_fail_no_mysql_row",
    (mysqlCreateLeft as any[]).length === 0,
    `mysql=${(mysqlCreateLeft as any[]).length}`,
  );

  // ---- Mongo fail: withdrawal create never touches MySQL ----
  delete process.env.DUAL_WRITE_FINANCIAL_FAIL;
  process.env.DUAL_WRITE_MONGO_FAIL = "1";
  const createIdW2 = createId();
  let createMongoFailThrew = false;
  try {
    await withFinancialDualWrite({
      site: "withdrawal.create",
      mongoWrite: async () =>
        WithdrawalRequest.create({
          _id: createIdW2,
          ownerId,
          amountTnd: 11,
          amountLyd: 15,
          exchangeRateRate: 1.4,
          method: "BANK_LYD",
          status: "PENDING",
          note: "fail-mongo-create",
        }),
      mysqlWrite: async (doc) => {
        await upsertWithdrawalMysql({
          id: String(doc._id),
          ownerId,
          amountTnd: 11,
          amountLyd: 15,
          method: "BANK_LYD",
          status: "PENDING",
        });
      },
      mongoCompensate: async (doc) => {
        await WithdrawalRequest.deleteOne({ _id: doc._id });
      },
    });
  } catch {
    createMongoFailThrew = true;
  }
  const mongoCreate2 = await WithdrawalRequest.findById(createIdW2).lean();
  const [mysqlCreate2] = await pool.query(
    `SELECT id FROM withdrawal_requests WHERE id = ?`,
    [createIdW2],
  );
  check("withdrawal_create_mongo_fail_throws", createMongoFailThrew, `threw=${createMongoFailThrew}`);
  check(
    "withdrawal_create_mongo_fail_neither_written",
    !mongoCreate2 && (mysqlCreate2 as any[]).length === 0,
    `mongo=${!!mongoCreate2} mysql=${(mysqlCreate2 as any[]).length}`,
  );

  // ---- Happy create then MySQL-fail on review (status reverts) ----
  delete process.env.DUAL_WRITE_MONGO_FAIL;
  delete process.env.DUAL_WRITE_FINANCIAL_FAIL;
  const reviewId = createId();
  await withFinancialDualWrite({
    site: "withdrawal.create",
    mongoWrite: async () =>
      WithdrawalRequest.create({
        _id: reviewId,
        ownerId,
        amountTnd: 12,
        amountLyd: 16,
        exchangeRateRate: 1.4,
        method: "BANK_LYD",
        status: "PENDING",
        note: "review-fail-setup",
      }),
    mysqlWrite: async (doc) => {
      await upsertWithdrawalMysql({
        id: String(doc._id),
        ownerId,
        amountTnd: 12,
        amountLyd: 16,
        exchangeRateRate: 1.4,
        method: "BANK_LYD",
        status: "PENDING",
        note: "review-fail-setup",
      });
    },
    mongoCompensate: async (doc) => {
      await WithdrawalRequest.deleteOne({ _id: doc._id });
    },
  });

  process.env.DUAL_WRITE_FINANCIAL_FAIL = "1";
  const row = await WithdrawalRequest.findById(reviewId);
  if (!row) throw new Error("setup withdrawal missing");
  const prev = {
    status: row.status,
    reviewedBy: row.reviewedBy,
    reviewedAt: row.reviewedAt,
    rejectionReason: row.rejectionReason,
  };
  let reviewFailThrew = false;
  try {
    await withFinancialDualWrite({
      site: "withdrawal.review",
      mongoWrite: async () => {
        row.status = "REJECTED";
        row.reviewedBy = ownerId;
        row.reviewedAt = new Date();
        row.rejectionReason = "selftest";
        await row.save();
        return row;
      },
      mysqlWrite: async (doc) => {
        await upsertWithdrawalMysql({
          id: String(doc._id),
          ownerId: doc.ownerId,
          amountTnd: Number(doc.amountTnd),
          amountLyd: Number(doc.amountLyd),
          method: doc.method,
          status: doc.status,
          rejectionReason: doc.rejectionReason || null,
          reviewedBy: doc.reviewedBy || null,
          reviewedAt: doc.reviewedAt || null,
        });
      },
      mongoCompensate: async (doc) => {
        doc.status = prev.status as any;
        doc.reviewedBy = prev.reviewedBy;
        doc.reviewedAt = prev.reviewedAt;
        doc.rejectionReason = prev.rejectionReason;
        await doc.save();
      },
    });
  } catch {
    reviewFailThrew = true;
  }
  const afterReview = await WithdrawalRequest.findById(reviewId).lean();
  const [mysqlReview] = await pool.query(
    `SELECT status FROM withdrawal_requests WHERE id = ?`,
    [reviewId],
  );
  check("withdrawal_review_mysql_fail_throws", reviewFailThrew, `threw=${reviewFailThrew}`);
  check(
    "withdrawal_review_mysql_fail_mongo_reverted",
    afterReview?.status === "PENDING",
    `status=${afterReview?.status}`,
  );
  check(
    "withdrawal_review_mysql_fail_mysql_still_pending",
    String((mysqlReview as any[])[0]?.status) === "PENDING",
    `mysqlStatus=${(mysqlReview as any[])[0]?.status}`,
  );

  // cleanup review fixture
  delete process.env.DUAL_WRITE_FINANCIAL_FAIL;
  await WithdrawalRequest.deleteOne({ _id: reviewId });
  await deleteWithdrawalMysql(reviewId).catch(() => undefined);

  // ---- Refund insert MySQL fail compensate (direct dual-write shape) ----
  process.env.DUAL_WRITE_FINANCIAL_FAIL = "1";
  const booking = await mongoose.connection.db!.collection("bookings").findOne({
    deletedAt: null,
    "payment.status": "PAID",
  });
  if (!booking) throw new Error("no paid booking for refund fail test");
  const refundId = createId();
  let refundMysqlFailThrew = false;
  try {
    await withFinancialDualWrite({
      site: "refund.insert",
      mongoWrite: async () =>
        Refund.create({
          _id: refundId,
          bookingId: String(booking._id),
          customerId: String(booking.customerId),
          ownerId: String(booking.ownerId),
          type: "PARTIAL",
          amountLyd: 1,
          amountTnd: 0.7,
          reasonCode: "ADMIN_ADJUSTMENT",
          source: "ADMIN",
          walletCreditedLyd: 0,
        }),
      mysqlWrite: async (doc) => {
        await insertRefundMysql({
          id: String(doc._id),
          bookingId: String(booking._id),
          customerId: String(booking.customerId),
          ownerId: String(booking.ownerId),
          type: "PARTIAL",
          amountLyd: 1,
          amountTnd: 0.7,
          reasonCode: "ADMIN_ADJUSTMENT",
          source: "ADMIN",
        });
      },
      mongoCompensate: async (doc) => {
        await Refund.deleteOne({ _id: doc._id });
      },
    });
  } catch {
    refundMysqlFailThrew = true;
  }
  const refundLeft = await Refund.findById(refundId).lean();
  const [mysqlRefund] = await pool.query(`SELECT id FROM refunds WHERE id = ?`, [refundId]);
  check("refund_mysql_fail_throws", refundMysqlFailThrew, `threw=${refundMysqlFailThrew}`);
  check("refund_mysql_fail_mongo_compensated", !refundLeft, `mongoLeft=${!!refundLeft}`);
  check(
    "refund_mysql_fail_no_mysql_row",
    (mysqlRefund as any[]).length === 0,
    `mysql=${(mysqlRefund as any[]).length}`,
  );

  // ---- Refund Mongo fail: neither written ----
  delete process.env.DUAL_WRITE_FINANCIAL_FAIL;
  process.env.DUAL_WRITE_MONGO_FAIL = "1";
  const refundId2 = createId();
  let refundMongoFailThrew = false;
  try {
    await withFinancialDualWrite({
      site: "refund.insert",
      mongoWrite: async () =>
        Refund.create({
          _id: refundId2,
          bookingId: String(booking._id),
          customerId: String(booking.customerId),
          ownerId: String(booking.ownerId),
          type: "PARTIAL",
          amountLyd: 1,
          amountTnd: 0.7,
          reasonCode: "ADMIN_ADJUSTMENT",
          source: "ADMIN",
        }),
      mysqlWrite: async (doc) => {
        await insertRefundMysql({
          id: String(doc._id),
          bookingId: String(booking._id),
          customerId: String(booking.customerId),
          ownerId: String(booking.ownerId),
          type: "PARTIAL",
          amountLyd: 1,
          amountTnd: 0.7,
          reasonCode: "ADMIN_ADJUSTMENT",
        });
      },
      mongoCompensate: async (doc) => {
        await Refund.deleteOne({ _id: doc._id });
      },
    });
  } catch {
    refundMongoFailThrew = true;
  }
  const refund2 = await Refund.findById(refundId2).lean();
  const [mysqlRefund2] = await pool.query(`SELECT id FROM refunds WHERE id = ?`, [refundId2]);
  check("refund_mongo_fail_throws", refundMongoFailThrew, `threw=${refundMongoFailThrew}`);
  check(
    "refund_mongo_fail_neither_written",
    !refund2 && (mysqlRefund2 as any[]).length === 0,
    `mongo=${!!refund2} mysql=${(mysqlRefund2 as any[]).length}`,
  );

  delete process.env.DUAL_WRITE_MONGO_FAIL;
  delete process.env.DUAL_WRITE_FINANCIAL_FAIL;

  const passed = results.filter((r) => r.pass).length;
  console.log(`\nSUMMARY dualwrite-withdrawals-refunds: ${passed}/${results.length} pass`);
  await pool.end();
  await disconnectMysql();
  await mongoose.disconnect();
  process.exit(passed === results.length ? 0 : 1);
}

main().catch(async (e) => {
  console.error(e instanceof Error ? e.message : e);
  delete process.env.DUAL_WRITE_MONGO_FAIL;
  delete process.env.DUAL_WRITE_FINANCIAL_FAIL;
  try {
    await disconnectMysql();
  } catch {
    /* ignore */
  }
  try {
    await mongoose.disconnect();
  } catch {
    /* ignore */
  }
  process.exit(1);
});
