import dayjs from "dayjs";
import { Cond } from "./Condition";
import { Fn } from "./Function";
import { Q } from "./Query";

const flavor = Q.flavors.mysql;

describe("Expression", () => {
  it("should produce valid SQL", () => {
    expect(Fn.count("*").toSQL(flavor)).toEqual("COUNT(*)");
    expect(Fn.count("foo").toSQL(flavor)).toEqual("COUNT(`foo`)");
    expect(Fn.sum("foo").toSQL(flavor)).toEqual("SUM(`foo`)");
    expect(Fn.max("foo").toSQL(flavor)).toEqual("MAX(`foo`)");
    expect(Fn.add("foo", "blah").toSQL(flavor)).toEqual("(`foo` + `blah`)");
    expect(Fn.subtract("foo", "blah").toSQL(flavor)).toEqual(
      "(`foo` - `blah`)"
    );
    expect(Fn.multiply("foo", "blah").toSQL(flavor)).toEqual(
      "(`foo` * `blah`)"
    );
    expect(Fn.divide("foo", "blah").toSQL(flavor)).toEqual("(`foo` / `blah`)");
    expect(Fn.ifnull("foo", Q.value(123)).toSQL(flavor)).toEqual(
      "IFNULL(`foo`,123)"
    );
    expect(Fn.ifnull("foo", Q.S`123`).toSQL(flavor)).toEqual(
      'IFNULL(`foo`,"123")'
    );
    expect(
      Fn.dateDiff("year", Q.value("2024-01-01"), Q.value("2025-01-01")).toSQL(
        flavor
      )
    ).toEqual('(YEAR("2024-01-01") - YEAR("2025-01-01"))');
    expect(Fn.substring("year", 1, 10).toSQL(flavor)).toEqual(
      "SUBSTRING(`year`,1,10)"
    );
  });
  it("should handle unary minus prefix on column names", () => {
    expect(Fn.sum("-amount_czk").toSQL(flavor)).toEqual("SUM(-`amount_czk`)");
    expect(Fn.sum("-amount").toSQL(flavor)).toEqual("SUM(-`amount`)");
    expect(Fn.max("-price").toSQL(flavor)).toEqual("MAX(-`price`)");
    expect(Fn.sum("-table.column").toSQL(flavor)).toEqual(
      "SUM(-`table`.`column`)"
    );
  });
  it("should handle empty string values", () => {
    expect(Q.S``).toBeDefined();
    const query = Q.select().addField(Q.S``).from("tableName");
    expect(query.toSQL(flavor)).toEqual('SELECT "" FROM `tableName`');

    // verify serialization round-trip
    const deserialized = Q.deserialize(query.serialize());
    expect(deserialized.toSQL(flavor)).toEqual(query.toSQL(flavor));
  });
  it("should support value composition", () => {
    expect(Fn.sum(Fn.ifnull("foo", Q.S`123`)).toSQL(flavor)).toEqual(
      'SUM(IFNULL(`foo`,"123"))'
    );
    expect(Fn.sum(Fn.ifnull("foo", Q.S`123`)).toSQL(flavor)).toEqual(
      'SUM(IFNULL(`foo`,"123"))'
    );
    expect(Fn.sum(Fn.ifnull("foo", `-123`)).toSQL(flavor)).toEqual(
      "SUM(IFNULL(`foo`,-123))"
    );
    expect(
      Fn.sum(Fn.if(Cond.equal("foo_blah", 123), "aa", 123)).toSQL(flavor)
    ).toEqual("SUM(IF(`foo_blah` = 123,`aa`,`123`))");
    expect(
      Fn.if(Cond.equal("foo_blah", 123), "aa", Q.expr(-123)).toSQL(flavor)
    ).toEqual("IF(`foo_blah` = 123,`aa`,-123)");
    expect(
      Fn.dateRangeSumField({
        dateColumn: "tax_date",
        valueColumn: "amount",
        start: dayjs("2020-01-01T00:00:00Z").startOf("day"),
        end: dayjs("2020-01-31T15:00:00Z").endOf("day"),
      }).toSQL(flavor)
    ).toEqual(
      'SUM(IF(`tax_date` BETWEEN "2020-01-01 00:00:00" AND "2020-01-31 23:59:59",`amount`,0))'
    );

    expect(
      Fn.sum(
        Fn.if(
          Cond.between(`tax_date`, ["2020-01-01", "2020-01-31"]),
          "price_last_year",
          Fn.string(`0`)
        )
      ).toSQL(flavor)
    ).toEqual(
      'SUM(IF(`tax_date` BETWEEN "2020-01-01" AND "2020-01-31",`price_last_year`,"0"))'
    );
  });
  it("should support serialization for functions", () => {
    const serialized =
      '{"type":"SelectQuery","tables":[],"unionQueries":[],"joins":[],"fields":[{"name":"SUM(YEAR(#foo#))"}],"where":[],"having":[],"orderBy":[],"groupBy":[]}';

    const fn = Q.select().addField(Fn.sum(Fn.year("foo")));
    const fn2 = Q.deserialize(serialized);
    // console.log("serialized test:", JSON.stringify(fn.serialize()));

    expect(fn2.toSQL(flavor)).toEqual(fn.toSQL(flavor));
  });
  it("should support serialization for operations", () => {
    const serialized =
      '{"type":"SelectQuery","tables":[],"unionQueries":[],"joins":[],"fields":[{"name":"((#foo# / #blah#) + #xyz#)"}],"where":[],"having":[],"orderBy":[],"groupBy":[]}';

    const fn = Q.select().addField(Fn.add(Fn.divide("foo", "blah"), "xyz"));
    const fn2 = Q.deserialize(serialized);
    // console.log("serialized test:", JSON.stringify(fn.serialize()));

    expect(fn2.toSQL(flavor)).toEqual(fn.toSQL(flavor));

    const fn3 = Q.select().addField(
      Fn.sum(
        Fn.if(
          Cond.between(`tax_date`, ["2020-01-01", "2020-01-31"]),
          "price_last_year",
          Fn.string(`0`)
        )
      ),
      "blah"
    );

    expect(Q.deserialize(fn3.serialize()).toSQL(flavor)).toEqual(
      fn3.toSQL(flavor)
    );

    const fn4 = Q.select().addField(
      Fn.priceCurrentAndPreviousDiffField({
        thisYearColumn: Fn.sum("price_this_year"),
        lastYearColumn: Fn.avg("price_last_year"),
      }),
      "blah"
    );
    expect(fn4.toSQL(flavor)).toEqual(
      "SELECT IF((SUM(`price_this_year`) = 0 AND AVG(`price_last_year`) = 0),+0,IF(AVG(`price_last_year`) = 0,NULL,IF(SUM(`price_this_year`) = 0,-1,((SUM(`price_this_year`) - AVG(`price_last_year`)) / ABS(AVG(`price_last_year`)))))) AS `blah` "
    );
    expect(Q.deserialize(fn4.serialize()).toSQL(flavor)).toEqual(
      fn4.toSQL(flavor)
    );
  });
  it("should support serialization for ifnull", () => {
    const fn = Q.select().addField(
      Fn.ifnull(Fn.if(Cond.equal("foo_blah", 123), "aa", Q.value(123)), "blah"),
      "blah"
    );
    expect(fn.toSQL(flavor)).toEqual(
      "SELECT IFNULL(IF(`foo_blah` = 123,`aa`,123),`blah`) AS `blah` "
    );
    expect(Q.deserialize(fn.serialize()).toSQL(flavor)).toEqual(
      fn.toSQL(flavor)
    );
  });
  it("datediff", () => {
    expect(Fn.dateDiff("day", "a", "b").toSQL(flavor)).toEqual(
      "DATEDIFF(`a`,`b`)"
    );
    expect(Fn.dateDiff("month", "a", "b").toSQL(flavor)).toEqual(
      "TIMESTAMPDIFF(MONTH, `a`, `b`)"
    );
    expect(Fn.dateDiff("year", "a", "b").toSQL(flavor)).toEqual(
      "(YEAR(`a`) - YEAR(`b`))"
    );
  });

  it("dateadd", () => {
    expect(Fn.dateAdd("date", 12, "year").toSQL(flavor)).toEqual(
      "DATE_ADD(`date`, INTERVAL 12 YEAR)"
    );
  });

  it("complex functions", () => {
    expect(Fn.if(Cond.lessThan("a", 30), "a", "b").toSQL(flavor)).toEqual(
      "IF(`a` < 30,`a`,`b`)"
    );
  });

  describe("concat", () => {
    it("should concatenate two values", () => {
      expect(Fn.concat("firstName", "lastName").toSQL(flavor)).toEqual(
        "CONCAT(`firstName`,`lastName`)"
      );
    });

    it("should concatenate multiple values", () => {
      expect(
        Fn.concat("firstName", Q.value(" "), "lastName").toSQL(flavor)
      ).toEqual('CONCAT(`firstName`," ",`lastName`)');
    });

    it("should concatenate with string literals", () => {
      expect(
        Fn.concat(Q.S`Hello`, Q.S` `, Q.S`World`).toSQL(flavor)
      ).toEqual('CONCAT("Hello"," ","World")');
    });

    it("should concatenate mixed types", () => {
      expect(
        Fn.concat("title", Q.S`: `, "description", Q.S` - `, Q.value(2024)).toSQL(
          flavor
        )
      ).toEqual('CONCAT(`title`,": ",`description`," - ",2024)');
    });

    it("should work with many arguments", () => {
      expect(
        Fn.concat("a", "b", "c", "d", "e").toSQL(flavor)
      ).toEqual("CONCAT(`a`,`b`,`c`,`d`,`e`)");
    });

    it("should work across different SQL flavors", () => {
      const mysqlFlavor = Q.flavors.mysql;
      const sqliteFlavor = Q.flavors.sqlite;

      expect(Fn.concat("a", "b").toSQL(mysqlFlavor)).toEqual(
        "CONCAT(`a`,`b`)"
      );
      expect(Fn.concat("a", "b").toSQL(sqliteFlavor)).toEqual(
        "CONCAT(`a`,`b`)"
      );
    });

    it("should support composition with other functions", () => {
      expect(
        Fn.concat(Fn.year("date"), Q.S`-`, Fn.month("date")).toSQL(
          flavor
        )
      ).toEqual('CONCAT(YEAR(`date`),"-",MONTH(`date`))');
    });

    it("should work inside other functions", () => {
      expect(
        Fn.ifnull(Fn.concat("firstName", Q.S` `, "lastName"), Q.S`Unknown`).toSQL(
          flavor
        )
      ).toEqual('IFNULL(CONCAT(`firstName`," ",`lastName`),"Unknown")');
    });

    it("should support serialization", () => {
      const query = Q.select().addField(
        Fn.concat("firstName", Q.S` `, "lastName"),
        "fullName"
      );
      const serialized = query.serialize();
      const deserialized = Q.deserialize(serialized);

      expect(deserialized.toSQL(flavor)).toEqual(query.toSQL(flavor));
      expect(deserialized.toSQL(flavor)).toEqual(
        'SELECT CONCAT(`firstName`," ",`lastName`) AS `fullName` '
      );
    });

    it("should serialize complex concat expressions", () => {
      const query = Q.select().addField(
        Fn.ifnull(
          Fn.concat(
            Fn.year("date"),
            Q.S`-`,
            Fn.month("date")
          ),
          Q.S`N/A`
        ),
        "displayName"
      );

      const serialized = query.serialize();
      const deserialized = Q.deserialize(serialized);

      expect(deserialized.toSQL(flavor)).toEqual(query.toSQL(flavor));
    });
  });
});

describe("SQL Injection Prevention", () => {
  it("should reject invalid DATEADD interval types", () => {
    const maliciousIntervalType = "year'); DROP TABLE users; --";
    expect(() => {
      Fn.dateAdd("date", 1, maliciousIntervalType as any).toSQL(flavor);
    }).toThrow("Invalid DATEADD interval type");
  });

  it("should sanitize DATEADD interval values to integers", () => {
    // Even with malicious input, parseInt will extract just the numeric portion
    // "1; DROP TABLE users" becomes 1
    const result = Fn.dateAdd("date", "1; DROP TABLE users" as any, "year").toSQL(flavor);
    expect(result).toEqual("DATE_ADD(`date`, INTERVAL 1 YEAR)");
    expect(result).not.toContain("DROP TABLE");
  });

  it("should reject non-numeric DATEADD interval values", () => {
    expect(() => {
      Fn.dateAdd("date", "abc" as any, "year").toSQL(flavor);
    }).toThrow("Invalid DATEADD interval");
  });

  it("should accept valid DATEADD interval types", () => {
    expect(Fn.dateAdd("date", 1, "year").toSQL(flavor)).toContain("YEAR");
    expect(Fn.dateAdd("date", 1, "month").toSQL(flavor)).toContain("MONTH");
    expect(Fn.dateAdd("date", 1, "day").toSQL(flavor)).toContain("DAY");
  });

  it("should validate DATEADD in SQLite flavor", () => {
    const sqliteFlavor = Q.flavors.sqlite;
    const maliciousIntervalType = "year'); DROP TABLE users; --";
    expect(() => {
      Fn.dateAdd("date", 1, maliciousIntervalType as any).toSQL(sqliteFlavor);
    }).toThrow("Invalid DATEADD interval type");
  });

  it("should validate DATEADD in AWS Timestream flavor", () => {
    const awsFlavor = Q.flavors.awsTimestream;
    const maliciousIntervalType = "year'); DROP TABLE users; --";
    expect(() => {
      Fn.dateAdd("date", 1, maliciousIntervalType as any).toSQL(awsFlavor);
    }).toThrow("Invalid DATEADD interval type");
  });
});
