import { beforeEach, describe, expect, it, vi } from "vitest";
import mockRequire from "mock-require";

const logMock = vi.fn();

let payAllModule;
let claimTaskModule;
let calculateTotalsModule;

const buildBaseJob = (overrides = {}) => ({
  id: "job-1",
  completed_tasks: [],
  rate_laa: 100,
  bodyshop: {
    id: "shop-1",
    md_responsibility_centers: {
      defaults: {
        costs: {
          LAA: "Body"
        }
      }
    },
    md_tasks_presets: {
      presets: []
    },
    employee_teams: []
  },
  joblines: [],
  timetickets: [],
  ...overrides
});

const buildReqRes = ({ job, body = {}, userEmail = "payroll@example.com" }) => {
  const client = {
    setHeaders: vi.fn().mockReturnThis(),
    request: vi.fn().mockResolvedValueOnce({ jobs_by_pk: job })
  };

  const req = {
    body: {
      jobid: job.id,
      ...body
    },
    user: {
      email: userEmail
    },
    BearerToken: "Bearer test",
    userGraphQLClient: client
  };

  const res = {
    json: vi.fn(),
    status: vi.fn().mockReturnThis()
  };

  return { client, req, res };
};

beforeEach(() => {
  vi.resetModules();
  vi.clearAllMocks();
  mockRequire.stopAll();
  mockRequire("../utils/logger", { log: logMock });
  payAllModule = require("./pay-all");
  claimTaskModule = require("./claim-task");
  calculateTotalsModule = require("./calculate-totals");
});

describe("payroll payout helpers", () => {
  it("defaults team members to hourly payout when no payout method is stored", () => {
    const { effectiveRate, payoutContext } = payAllModule.BuildPayoutDetails(
      {},
      {
        labor_rates: {
          LAA: 27.5
        },
        employee: {
          id: "emp-1"
        }
      },
      "LAA"
    );

    expect(effectiveRate).toBe(27.5);
    expect(payoutContext).toEqual(
      expect.objectContaining({
        payout_type: "hourly",
        payout_method: "hourly",
        cut_percent_applied: null,
        source_labor_rate: null,
        source_labor_type: "LAA",
        effective_rate: 27.5
      })
    );
  });

  it("calculates commission payout rates from the raw job labor sale rate", () => {
    const { effectiveRate, payoutContext } = payAllModule.BuildPayoutDetails(
      {
        rate_laa: 120
      },
      {
        payout_method: "commission",
        commission_rates: {
          LAA: 35
        },
        employee: {
          id: "emp-1"
        }
      },
      "LAA"
    );

    expect(effectiveRate).toBe(42);
    expect(payoutContext).toEqual(
      expect.objectContaining({
        payout_type: "cut",
        payout_method: "commission",
        cut_percent_applied: 35,
        source_labor_rate: 120,
        source_labor_type: "LAA",
        effective_rate: 42
      })
    );
  });

  it("uses Dinero half-even rounding for stored hourly rates", () => {
    const { effectiveRate, payoutContext } = payAllModule.BuildPayoutDetails(
      {},
      {
        labor_rates: {
          LAA: 10.005
        },
        employee: {
          id: "emp-1"
        }
      },
      "LAA"
    );

    expect(effectiveRate).toBe(10);
    expect(payoutContext.effective_rate).toBe(10);
  });

  it("throws a useful error when commission configuration is incomplete", () => {
    expect(() =>
      payAllModule.BuildPayoutDetails(
        {
          rate_laa: 100
        },
        {
          payout_method: "commission",
          commission_rates: {},
          employee: {
            first_name: "Jane",
            last_name: "Doe"
          }
        },
        "LAA"
      )
    ).toThrow("Missing commission percent for Jane Doe on labor type LAA.");
  });

  it("throws a useful error when an hourly payout rate is missing", () => {
    expect(() =>
      payAllModule.BuildPayoutDetails(
        {},
        {
          labor_rates: {},
          employee: {
            first_name: "John",
            last_name: "Smith"
          }
        },
        "LAB"
      )
    ).toThrow("Missing hourly payout rate for John Smith on labor type LAB.");
  });

  it("supports commission boundary values of zero and one hundred percent", () => {
    const zeroPercent = payAllModule.BuildPayoutDetails(
      {
        rate_laa: 123.45
      },
      {
        payout_method: "commission",
        commission_rates: {
          LAA: 0
        },
        employee: {
          id: "emp-1"
        }
      },
      "LAA"
    );

    const fullPercent = payAllModule.BuildPayoutDetails(
      {
        rate_laa: 123.45
      },
      {
        payout_method: "commission",
        commission_rates: {
          LAA: 100
        },
        employee: {
          id: "emp-1"
        }
      },
      "LAA"
    );

    expect(zeroPercent.effectiveRate).toBe(0);
    expect(zeroPercent.payoutContext.cut_percent_applied).toBe(0);
    expect(fullPercent.effectiveRate).toBe(123.45);
    expect(fullPercent.payoutContext.cut_percent_applied).toBe(100);
  });

  it("throws a useful error when the sale rate for a commission labor type is missing", () => {
    expect(() =>
      payAllModule.BuildPayoutDetails(
        {},
        {
          payout_method: "commission",
          commission_rates: {
            LAA: 35
          },
          employee: {
            first_name: "Sam",
            last_name: "Painter"
          }
        },
        "LAA"
      )
    ).toThrow("Missing sale rate rate_laa for labor type LAA.");
  });

  it("rejects commission percentages outside the allowed zero-to-one-hundred range", () => {
    expect(() =>
      payAllModule.BuildPayoutDetails(
        {
          rate_laa: 100
        },
        {
          payout_method: "commission",
          commission_rates: {
            LAA: -5
          },
          employee: {
            first_name: "Alex",
            last_name: "Painter"
          }
        },
        "LAA"
      )
    ).toThrow("Commission percent for Alex Painter on labor type LAA must be between 0 and 100.");

    expect(() =>
      payAllModule.BuildPayoutDetails(
        {
          rate_laa: 100
        },
        {
          payout_method: "commission",
          commission_rates: {
            LAA: 105
          },
          employee: {
            first_name: "Alex",
            last_name: "Painter"
          }
        },
        "LAA"
      )
    ).toThrow("Commission percent for Alex Painter on labor type LAA must be between 0 and 100.");
  });
});

describe("payroll routes", () => {
  it("aggregates claimed hours across prior ticket rates and inserts the remaining delta at the current rate", async () => {
    const job = buildBaseJob({
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAA: "Body"
            }
          }
        },
        md_tasks_presets: {
          presets: []
        },
        employee_teams: [
          {
            id: "team-1",
            employee_team_members: [
              {
                percentage: 100,
                payout_method: "commission",
                commission_rates: {
                  LAA: 40
                },
                labor_rates: {
                  LAA: 30
                },
                employee: {
                  id: "emp-1",
                  first_name: "Jane",
                  last_name: "Doe"
                }
              }
            ]
          }
        ]
      },
      joblines: [
        {
          mod_lbr_ty: "LAA",
          mod_lb_hrs: 10,
          assigned_team: "team-1",
          convertedtolbr: false
        }
      ],
      timetickets: [
        {
          employeeid: "emp-1",
          ciecacode: "LAA",
          productivehrs: 2,
          rate: 30,
          payout_context: {
            payout_method: "hourly"
          }
        },
        {
          employeeid: "emp-1",
          ciecacode: "LAA",
          productivehrs: 3,
          rate: 35,
          payout_context: {
            payout_method: "commission"
          }
        }
      ]
    });

    const { client, req, res } = buildReqRes({ job });
    client.request.mockResolvedValueOnce({ insert_timetickets: { affected_rows: 1 } });

    await payAllModule.payall(req, res);

    expect(client.request).toHaveBeenCalledTimes(2);

    const insertedTickets = client.request.mock.calls[1][1].timetickets;
    expect(insertedTickets).toHaveLength(1);
    expect(insertedTickets[0]).toEqual(
      expect.objectContaining({
        task_name: "Pay All",
        employeeid: "emp-1",
        productivehrs: 5,
        rate: 40,
        ciecacode: "LAA",
        cost_center: "Body",
        created_by: "payroll@example.com"
      })
    );
    expect(insertedTickets[0].payout_context).toEqual(
      expect.objectContaining({
        payout_method: "commission",
        cut_percent_applied: 40,
        source_labor_rate: 100,
        generated_from: "payall",
        task_name: "Pay All",
        used_ticket_fallback: false
      })
    );
    expect(res.json).toHaveBeenCalledWith(insertedTickets);
  });

  it("returns a validation failure when job lines still have unassigned hours", async () => {
    const job = buildBaseJob({
      joblines: [
        {
          mod_lbr_ty: "LAA",
          mod_lb_hrs: 3.5,
          assigned_team: null,
          convertedtolbr: false
        }
      ]
    });

    const { client, req, res } = buildReqRes({ job });

    await payAllModule.payall(req, res);

    expect(client.request).toHaveBeenCalledTimes(1);
    expect(res.json).toHaveBeenCalledWith({
      success: false,
      error: "Not all hours have been assigned."
    });
  });

  it("creates separate pay-all tickets for mixed hourly and commission team members across labor types", async () => {
    const job = buildBaseJob({
      rate_laa: 100,
      rate_lab: 80,
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAA: "Body",
              LAB: "Refinish"
            }
          }
        },
        md_tasks_presets: {
          presets: []
        },
        employee_teams: [
          {
            id: "team-1",
            employee_team_members: [
              {
                percentage: 50,
                payout_method: "hourly",
                labor_rates: {
                  LAA: 30,
                  LAB: 25
                },
                employee: {
                  id: "emp-hourly",
                  first_name: "Hourly",
                  last_name: "Tech"
                }
              },
              {
                percentage: 50,
                payout_method: "commission",
                commission_rates: {
                  LAA: 40,
                  LAB: 50
                },
                labor_rates: {
                  LAA: 0,
                  LAB: 0
                },
                employee: {
                  id: "emp-commission",
                  first_name: "Commission",
                  last_name: "Tech"
                }
              }
            ]
          }
        ]
      },
      joblines: [
        {
          mod_lbr_ty: "LAA",
          mod_lb_hrs: 4,
          assigned_team: "team-1",
          convertedtolbr: false
        },
        {
          mod_lbr_ty: "LAB",
          mod_lb_hrs: 2,
          assigned_team: "team-1",
          convertedtolbr: false
        }
      ]
    });

    const { client, req, res } = buildReqRes({ job });
    client.request.mockResolvedValueOnce({ insert_timetickets: { affected_rows: 4 } });

    await payAllModule.payall(req, res);

    expect(client.request).toHaveBeenCalledTimes(2);

    const insertedTickets = client.request.mock.calls[1][1].timetickets;
    expect(insertedTickets).toHaveLength(4);
    expect(insertedTickets).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          employeeid: "emp-hourly",
          ciecacode: "LAA",
          productivehrs: 2,
          rate: 30,
          cost_center: "Body",
          payout_context: expect.objectContaining({
            payout_method: "hourly",
            payout_type: "hourly",
            effective_rate: 30
          })
        }),
        expect.objectContaining({
          employeeid: "emp-hourly",
          ciecacode: "LAB",
          productivehrs: 1,
          rate: 25,
          cost_center: "Refinish",
          payout_context: expect.objectContaining({
            payout_method: "hourly",
            payout_type: "hourly",
            effective_rate: 25
          })
        }),
        expect.objectContaining({
          employeeid: "emp-commission",
          ciecacode: "LAA",
          productivehrs: 2,
          rate: 40,
          cost_center: "Body",
          payout_context: expect.objectContaining({
            payout_method: "commission",
            payout_type: "cut",
            cut_percent_applied: 40,
            source_labor_rate: 100,
            effective_rate: 40
          })
        }),
        expect.objectContaining({
          employeeid: "emp-commission",
          ciecacode: "LAB",
          productivehrs: 1,
          rate: 40,
          cost_center: "Refinish",
          payout_context: expect.objectContaining({
            payout_method: "commission",
            payout_type: "cut",
            cut_percent_applied: 50,
            source_labor_rate: 80,
            effective_rate: 40
          })
        })
      ])
    );
    expect(res.json).toHaveBeenCalledWith(insertedTickets);
  });

  it("creates a negative pay-all adjustment at the current commission rate when the remaining expected hours drop below prior claimed hours", async () => {
    const job = buildBaseJob({
      rate_laa: 120,
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAA: "Body"
            }
          }
        },
        md_tasks_presets: {
          presets: []
        },
        employee_teams: [
          {
            id: "team-1",
            employee_team_members: [
              {
                percentage: 100,
                payout_method: "commission",
                commission_rates: {
                  LAA: 40
                },
                employee: {
                  id: "emp-1",
                  first_name: "Current",
                  last_name: "Tech"
                }
              }
            ]
          }
        ]
      },
      joblines: [
        {
          mod_lbr_ty: "LAA",
          mod_lb_hrs: 4,
          assigned_team: "team-1",
          convertedtolbr: false
        }
      ],
      timetickets: [
        {
          employeeid: "emp-1",
          ciecacode: "LAA",
          productivehrs: 6,
          rate: 30,
          payout_context: {
            payout_method: "hourly",
            payout_type: "hourly",
            effective_rate: 30
          }
        }
      ]
    });

    const { client, req, res } = buildReqRes({ job });
    client.request.mockResolvedValueOnce({ insert_timetickets: { affected_rows: 1 } });

    await payAllModule.payall(req, res);

    const insertedTickets = client.request.mock.calls[1][1].timetickets;
    expect(insertedTickets).toHaveLength(1);
    expect(insertedTickets[0]).toEqual(
      expect.objectContaining({
        employeeid: "emp-1",
        productivehrs: -2,
        rate: 48,
        ciecacode: "LAA",
        memo: "Adjust flagged hours per assignment. (payroll@example.com)"
      })
    );
    expect(insertedTickets[0].payout_context).toEqual(
      expect.objectContaining({
        payout_method: "commission",
        payout_type: "cut",
        cut_percent_applied: 40,
        source_labor_rate: 120,
        effective_rate: 48,
        generated_from: "payall",
        used_ticket_fallback: false
      })
    );
    expect(res.json).toHaveBeenCalledWith(insertedTickets);
  });

  it("uses the current commission sale rate for remaining hours when older commission tickets were created from a lower sale rate", async () => {
    const job = buildBaseJob({
      rate_laa: 120,
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAA: "Body"
            }
          }
        },
        md_tasks_presets: {
          presets: []
        },
        employee_teams: [
          {
            id: "team-1",
            employee_team_members: [
              {
                percentage: 100,
                payout_method: "commission",
                commission_rates: {
                  LAA: 40
                },
                employee: {
                  id: "emp-1",
                  first_name: "Current",
                  last_name: "Tech"
                }
              }
            ]
          }
        ]
      },
      joblines: [
        {
          mod_lbr_ty: "LAA",
          mod_lb_hrs: 5,
          assigned_team: "team-1",
          convertedtolbr: false
        }
      ],
      timetickets: [
        {
          employeeid: "emp-1",
          ciecacode: "LAA",
          productivehrs: 2,
          rate: 40,
          payout_context: {
            payout_method: "commission",
            payout_type: "cut",
            cut_percent_applied: 40,
            source_labor_rate: 100,
            effective_rate: 40
          }
        }
      ]
    });

    const { client, req, res } = buildReqRes({ job });
    client.request.mockResolvedValueOnce({ insert_timetickets: { affected_rows: 1 } });

    await payAllModule.payall(req, res);

    const insertedTickets = client.request.mock.calls[1][1].timetickets;
    expect(insertedTickets).toHaveLength(1);
    expect(insertedTickets[0]).toEqual(
      expect.objectContaining({
        employeeid: "emp-1",
        productivehrs: 3,
        rate: 48,
        ciecacode: "LAA"
      })
    );
    expect(insertedTickets[0].payout_context).toEqual(
      expect.objectContaining({
        payout_method: "commission",
        cut_percent_applied: 40,
        source_labor_rate: 120,
        effective_rate: 48,
        generated_from: "payall",
        used_ticket_fallback: false
      })
    );
    expect(res.json).toHaveBeenCalledWith(insertedTickets);
  });

  it("rejects duplicate claim-task submissions for completed presets", async () => {
    const job = buildBaseJob({
      completed_tasks: [{ name: "Disassembly" }],
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAA: "Body"
            }
          }
        },
        md_tasks_presets: {
          presets: [
            {
              name: "Disassembly",
              hourstype: ["LAA"],
              percent: 50,
              nextstatus: "In Progress",
              memo: "Flag disassembly"
            }
          ]
        },
        employee_teams: []
      }
    });

    const { client, req, res } = buildReqRes({
      job,
      body: {
        task: "Disassembly",
        calculateOnly: false,
        employee: {
          name: "Jane Doe",
          employeeid: "emp-1"
        }
      }
    });

    await claimTaskModule.claimtask(req, res);

    expect(client.request).toHaveBeenCalledTimes(1);
    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledWith({
      success: false,
      error: "Provided task preset has already been completed for this job."
    });
  });

  it("rejects claim-task when task presets over-allocate the same labor type", async () => {
    const job = buildBaseJob({
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAA: "Body"
            }
          }
        },
        md_tasks_presets: {
          presets: [
            {
              name: "Body Prep",
              hourstype: ["LAA"],
              percent: 60,
              nextstatus: "Prep",
              memo: "Prep body work"
            },
            {
              name: "Body Prime",
              hourstype: ["LAA"],
              percent: 50,
              nextstatus: "Prime",
              memo: "Prime body work"
            }
          ]
        },
        employee_teams: []
      }
    });

    const { client, req, res } = buildReqRes({
      job,
      body: {
        task: "Body Prep",
        calculateOnly: true,
        employee: {
          name: "Jane Doe",
          employeeid: "emp-1"
        }
      }
    });

    await claimTaskModule.claimtask(req, res);

    expect(client.request).toHaveBeenCalledTimes(1);
    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledWith({
      success: false,
      error: "Task preset percentages for labor type LAA total 110% and cannot exceed 100%."
    });
  });

  it("returns commission-aware claim-task previews and reports unassigned hours", async () => {
    const job = buildBaseJob({
      rate_laa: 120,
      rate_lab: 80,
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAA: "Body",
              LAB: "Refinish"
            }
          }
        },
        md_tasks_presets: {
          presets: [
            {
              name: "Body Prep",
              hourstype: ["LAA", "LAB"],
              percent: 50,
              nextstatus: "Prep",
              memo: "Prep body work"
            }
          ]
        },
        employee_teams: [
          {
            id: "team-1",
            employee_team_members: [
              {
                percentage: 50,
                payout_method: "hourly",
                labor_rates: {
                  LAA: 30,
                  LAB: 25
                },
                employee: {
                  id: "emp-hourly",
                  first_name: "Hourly",
                  last_name: "Tech"
                }
              },
              {
                percentage: 50,
                payout_method: "commission",
                commission_rates: {
                  LAA: 40,
                  LAB: 50
                },
                employee: {
                  id: "emp-commission",
                  first_name: "Commission",
                  last_name: "Tech"
                }
              }
            ]
          }
        ]
      },
      joblines: [
        {
          mod_lbr_ty: "LAA",
          mod_lb_hrs: 4,
          assigned_team: "team-1",
          convertedtolbr: false
        },
        {
          mod_lbr_ty: "LAB",
          mod_lb_hrs: 1.5,
          assigned_team: null,
          convertedtolbr: false
        }
      ]
    });

    const { client, req, res } = buildReqRes({
      job,
      body: {
        task: "Body Prep",
        calculateOnly: true,
        employee: {
          name: "Payroll Manager"
        }
      }
    });

    await claimTaskModule.claimtask(req, res);

    expect(client.request).toHaveBeenCalledTimes(1);
    expect(res.json).toHaveBeenCalledWith({
      unassignedHours: 1.5,
      ticketsToInsert: expect.arrayContaining([
        expect.objectContaining({
          task_name: "Body Prep",
          employeeid: "emp-hourly",
          productivehrs: 1,
          rate: 30,
          ciecacode: "LAA",
          created_by: "Payroll Manager",
          payout_context: expect.objectContaining({
            payout_method: "hourly",
            payout_type: "hourly",
            generated_from: "claimtask",
            task_name: "Body Prep"
          })
        }),
        expect.objectContaining({
          task_name: "Body Prep",
          employeeid: "emp-commission",
          productivehrs: 1,
          rate: 48,
          ciecacode: "LAA",
          created_by: "Payroll Manager",
          payout_context: expect.objectContaining({
            payout_method: "commission",
            payout_type: "cut",
            cut_percent_applied: 40,
            source_labor_rate: 120,
            generated_from: "claimtask",
            task_name: "Body Prep"
          })
        })
      ])
    });
  });

  it("rejects claim-task when an assigned team member is missing the hourly rate for the selected labor type", async () => {
    const job = buildBaseJob({
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAB: "Body"
            }
          }
        },
        md_tasks_presets: {
          presets: [
            {
              name: "Teardown",
              hourstype: ["LAB"],
              percent: 100,
              nextstatus: "In Progress",
              memo: "Teardown"
            }
          ]
        },
        employee_teams: [
          {
            id: "team-1",
            employee_team_members: [
              {
                percentage: 50,
                labor_rates: {
                  LAB: 45
                },
                employee: {
                  id: "emp-1",
                  first_name: "Configured",
                  last_name: "Tech"
                }
              },
              {
                percentage: 50,
                labor_rates: {},
                employee: {
                  id: "emp-2",
                  first_name: "Missing",
                  last_name: "Rate"
                }
              }
            ]
          }
        ]
      },
      joblines: [
        {
          mod_lbr_ty: "LAB",
          mod_lb_hrs: 4.4,
          assigned_team: "team-1",
          convertedtolbr: false
        }
      ]
    });

    const { client, req, res } = buildReqRes({
      job,
      body: {
        task: "Teardown",
        calculateOnly: true,
        employee: {
          name: "Dave",
          email: "dave@rome.test"
        }
      }
    });

    await claimTaskModule.claimtask(req, res);

    expect(client.request).toHaveBeenCalledTimes(1);
    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledWith({
      success: false,
      error: "Missing hourly payout rate for Missing Rate on labor type LAB."
    });
  });

  it("locks in the current enhanced-payroll behavior of ignoring lbr_adjustments when calculating labor totals", async () => {
    const job = buildBaseJob({
      lbr_adjustments: {
        LAA: 2.5
      },
      bodyshop: {
        id: "shop-1",
        md_responsibility_centers: {
          defaults: {
            costs: {
              LAA: "Body"
            }
          }
        },
        md_tasks_presets: {
          presets: []
        },
        employee_teams: [
          {
            id: "team-1",
            employee_team_members: [
              {
                percentage: 100,
                payout_method: "commission",
                commission_rates: {
                  LAA: 40
                },
                employee: {
                  id: "emp-1",
                  first_name: "Current",
                  last_name: "Tech"
                }
              }
            ]
          }
        ]
      },
      joblines: [
        {
          mod_lbr_ty: "LAA",
          mod_lb_hrs: 4,
          assigned_team: "team-1",
          convertedtolbr: false
        }
      ],
      timetickets: [
        {
          employeeid: "emp-1",
          ciecacode: "LAA",
          productivehrs: 1,
          rate: 48,
          payout_context: {
            payout_method: "commission"
          }
        }
      ]
    });

    const { client, req, res } = buildReqRes({ job });

    await calculateTotalsModule.calculatelabor(req, res);

    expect(client.request).toHaveBeenCalledTimes(1);
    expect(res.json).toHaveBeenCalledWith([
      {
        employeeid: "emp-1",
        rate: 40,
        mod_lbr_ty: "LAA",
        expectedHours: 4,
        claimedHours: 1
      }
    ]);
  });
});
