## Description #############################################################################
#
# Excel Back End: Test excel_formatters
#
############################################################################################

@testset "Excel Formatters" verbose = true begin

    # == Numeric and Date Formatters =======================================================

    @testset "Numeric Formatters" verbose = true begin
        matrix = [
            π π π π
            π π π π
        ]

        # The Excel rows are laid out as follows:
        #
        #   Row 1: column labels
        #   Row 2: data row 1     (data row index 1)
        #   Row 3: data row 2     (data row index 2)
        #   Row 4: summary row 1  (summary row index 1)
        #   Row 5: summary row 2  (summary row index 2)
        #
        # A `:data` formatter (the default) matches the data row index (`i ∈ {1, 2}`),
        # whereas a `:summary_row` formatter matches the summary row index (`i ∈ {1, 2}`),
        # NOT the Excel row index. Using two summary rows ensures we are indexing by the
        # summary row, not by the absolute Excel row (which would be 4 and 5).
        result = pretty_table(
            XLSX.XLSXFile,
            matrix;
            excel_formatters = [
                # `:data` region matching a specific data row index.
                ExcelFormatter((v, i, j) -> (i == 2), ["format" => "0.00"])
                # `:summary_row` region matching the summary row index.
                ExcelFormatter(
                    (v, i, j) -> (i == 1), ["format" => "0.00000"]; region = :summary_row
                )
                ExcelFormatter(
                    (v, i, j) -> (i == 2), ["format" => "0.000"]; region = :summary_row
                )
                # `:data` region matching by column.
                ExcelFormatter((v, i, j) -> (j == 1), ["format" => "#,##0_0_0"])
                ExcelFormatter((v, i, j) -> (j == 2), ["format" => "#,##0.??_0_0"])
                ExcelFormatter((v, i, j) -> (j == 3), ["format" => "#,##0.???"])
                ExcelFormatter((v, i, j) -> (j == 4), ["format" => "0_0_0_0"])
            ],
            summary_row_labels = ["Maximum", "Minimum"],
            summary_rows = [
                (data, i) -> maximum(@views data[1:2, i]),
                (data, i) -> minimum(@views data[1:2, i]),
            ],
        )

        # Data row 1 (Excel row 2): matched by the column formatters.
        @test XLSX.getFormat(result[1], "B2").format["numFmt"]["formatCode"] == "#,##0_0_0"
        @test XLSX.getFormat(result[1], "C2").format["numFmt"]["formatCode"] ==
            "#,##0.??_0_0"
        @test XLSX.getFormat(result[1], "D2").format["numFmt"]["formatCode"] == "#,##0.???"
        @test XLSX.getFormat(result[1], "E2").format["numFmt"]["formatCode"] == "0_0_0_0"

        # Data row 2 (Excel row 3): matched by the `:data` formatter `i == 2`, which takes
        # precedence over the column formatters because it appears first.
        @test XLSX.getFormat(result[1], "B3").format["numFmt"]["formatCode"] == "0.00"
        @test XLSX.getFormat(result[1], "E3").format["numFmt"]["formatCode"] == "0.00"

        # Summary row 1 (Excel row 4): matched by the `:summary_row` formatter `i == 1`. The
        # `:data` column formatters must NOT leak into the summary rows.
        @test XLSX.getFormat(result[1], "B4").format["numFmt"]["formatCode"] == "0.00000"
        @test XLSX.getFormat(result[1], "D4").format["numFmt"]["formatCode"] == "0.00000"

        # Summary row 2 (Excel row 5): matched by the `:summary_row` formatter `i == 2`,
        # confirming the index is the summary row index and not the Excel row index.
        @test XLSX.getFormat(result[1], "B5").format["numFmt"]["formatCode"] == "0.000"
        @test XLSX.getFormat(result[1], "D5").format["numFmt"]["formatCode"] == "0.000"
    end

    # == Predicate receives the entire data matrix =======================================

    @testset "Predicate receives the data matrix" verbose = true begin
        # The `ExcelFormatter` predicate signature is `f(data, i, j)`, where the first
        # argument is the entire data matrix passed to `pretty_table`. The predicate can
        # therefore index into `data` to express conditions on the cell value.
        matrix = [
            1 2 3
            4 5 6
            7 8 9
        ]

        result = pretty_table(
            XLSX.XLSXFile,
            matrix;
            excel_formatters = [
                ExcelFormatter((data, i, j) -> (data[i, j] > 5), ["format" => "0.00"])
            ],
        )

        # Cells whose value exceeds 5 are: (2,3) = 6 and (3,*) = {7,8,9}. Excel rows are
        # one header row plus the data rows, so the matched cells sit at C3, A4, B4, C4.
        @test XLSX.getFormat(result[1], "C3").format["numFmt"]["formatCode"] == "0.00"
        @test XLSX.getFormat(result[1], "A4").format["numFmt"]["formatCode"] == "0.00"
        @test XLSX.getFormat(result[1], "B4").format["numFmt"]["formatCode"] == "0.00"
        @test XLSX.getFormat(result[1], "C4").format["numFmt"]["formatCode"] == "0.00"
        # Cells whose value does not exceed 5 must keep the default "General" format.
        @test XLSX.getFormat(result[1], "A2").format["numFmt"]["formatCode"] == "General"
        @test XLSX.getFormat(result[1], "B2").format["numFmt"]["formatCode"] == "General"
        @test XLSX.getFormat(result[1], "C2").format["numFmt"]["formatCode"] == "General"
    end

    # == Formats Must Survive the Cell Value Assignment ====================================

    @testset "Formats Survive the Value Assignment" verbose = true begin
        # Assigning an `Int64` or `Float64` value to a cell resets its number format to
        # "General" unless XLSX.jl considers the current format float-like (e.g. "0.00").
        # Hence, the formatters must be applied only after the rendered cell is assigned to
        # the sheet (see issue #318). This regression requires numeric data cells together
        # with integer-like formats because string cells never reset the format and
        # float-like formats survive the assignment.
        matrix = Any[
            1000000.3 5
            2000000.5 10
        ]

        result = pretty_table(
            XLSX.XLSXFile,
            matrix;
            excel_formatters = [
                # Integer-like format on a `Float64` column.
                ExcelFormatter((v, i, j) -> (j == 1), ["format" => "#,##0"])
                # Integer-like format on an `Int64` column.
                ExcelFormatter((v, i, j) -> (j == 2), ["format" => "#,##0_0"])
                # Integer-like format on the summary row, whose cells are also numeric.
                ExcelFormatter(
                    (v, i, j) -> true, ["format" => "#,##0"]; region = :summary_row
                )
            ],
            summary_row_labels = ["Total"],
            summary_rows = [(data, i) -> sum(@views data[:, i])],
        )

        # Data rows (Excel rows 2 and 3).
        @test XLSX.getFormat(result[1], "B2").format["numFmt"]["formatCode"] == "#,##0"
        @test XLSX.getFormat(result[1], "B3").format["numFmt"]["formatCode"] == "#,##0"
        @test XLSX.getFormat(result[1], "C2").format["numFmt"]["formatCode"] == "#,##0_0"
        @test XLSX.getFormat(result[1], "C3").format["numFmt"]["formatCode"] == "#,##0_0"

        # Summary row (Excel row 4).
        @test XLSX.getFormat(result[1], "B4").format["numFmt"]["formatCode"] == "#,##0"
        @test XLSX.getFormat(result[1], "C4").format["numFmt"]["formatCode"] == "#,##0"
    end

    @testset "Issue 318 Reproduction" verbose = true begin
        # Mirror the reproduction in issue #318, which uses a column table with `Float64`
        # columns and both integer-like and float-like formats.
        table = (
            CMA                   = ["CMA1", "CMA2", "CMA3", "CMA4"],
            Constituents          = [5, 10, 15, 20],
            Total_amount          = [1000000.3, 2000000.5, 3000000.7, 4000001.9],
            Total_Population      = [50000.7, 100000.5, 150000.3, 200000.1],
            Per_Capita            = [20.2, 20.3, 20.5, 20.6],
            Multiple_UK_PerCapita = [1.0, 1.2, 1.4, 1.6],
        )

        result = pretty_table(
            XLSX.XLSXFile,
            table;
            excel_formatters = [
                ExcelFormatter((v, i, j) -> (j == 3), ["format" => "#,##0"])
                ExcelFormatter((v, i, j) -> (j == 4), ["format" => "#,##0.0"])
                ExcelFormatter((v, i, j) -> (j == 5), ["format" => "#,##0"])
                ExcelFormatter((v, i, j) -> (j == 6), ["format" => "#,##0.00"])
            ],
        )

        # Column tables render two column label rows (the column names and the column
        # types), so the data cells start at the third Excel row.
        for row in 3:6
            @test XLSX.getFormat(result[1], "C$row").format["numFmt"]["formatCode"] ==
                "#,##0"
            @test XLSX.getFormat(result[1], "D$row").format["numFmt"]["formatCode"] ==
                "#,##0.0"
            @test XLSX.getFormat(result[1], "E$row").format["numFmt"]["formatCode"] ==
                "#,##0"
            @test XLSX.getFormat(result[1], "F$row").format["numFmt"]["formatCode"] ==
                "#,##0.00"
        end

        # Columns without a matching formatter must keep the default "General" format.
        @test XLSX.getFormat(result[1], "A3").format["numFmt"]["formatCode"] == "General"
        @test XLSX.getFormat(result[1], "B3").format["numFmt"]["formatCode"] == "General"
    end

    # == Numeric Format Codes ==============================================================

    @testset "Numeric Format Codes" verbose = true begin
        # Purely numeric format strings must reach `XLSX.setFormat` as strings, where they
        # select a format code like "0" or a built-in format ID like "39". Parsing them to
        # `Int`, as done with the other attributes, makes `XLSX.setFormat` throw.
        matrix = [
            1234.5 1234.5
            1234.5 1234.5
        ]

        result = pretty_table(
            XLSX.XLSXFile,
            matrix;
            excel_formatters = [
                # The format code "0" must be applied verbatim.
                ExcelFormatter((v, i, j) -> (j == 1), ["format" => "0"])
                # The format "39" selects the built-in format ID 39.
                ExcelFormatter((v, i, j) -> (j == 2), ["format" => "39"])
            ],
        )

        @test XLSX.getFormat(result[1], "A2").format["numFmt"]["formatCode"] == "0"
        @test XLSX.getFormat(result[1], "A3").format["numFmt"]["formatCode"] == "0"
        @test XLSX.getFormat(result[1], "B2").format["numFmt"]["formatCode"] ==
            "#,##0.00_);(#,##0.00)"
        @test XLSX.getFormat(result[1], "B3").format["numFmt"]["formatCode"] ==
            "#,##0.00_);(#,##0.00)"
    end

    # == Region Field and Constructors =====================================================
    @testset "Region Field and Constructors" verbose = true begin
        # The default region must be `:data`.
        f = ExcelFormatter((v, i, j) -> true, ["format" => "0.00"])
        @test f.region === :data

        # The region can be set explicitly.
        f = ExcelFormatter((v, i, j) -> true, ["format" => "0.00"]; region = :summary_row)
        @test f.region === :summary_row

        # An invalid region must throw.
        @test_throws ArgumentError ExcelFormatter(
            (v, i, j) -> true, ["format" => "0.00"]; region = :invalid
        )
    end

    # == Date Formatters ===================================================================

    @testset "Date Formatters" verbose = true begin
        now = Dates.now()
        matrix = [
            now now now now
            now now now now
        ]

        result = pretty_table(
            XLSX.XLSXFile,
            matrix;
            excel_formatters = [
                ExcelFormatter((v, i, j) -> (j == 1), ["format" => "ShortDate"])
                ExcelFormatter((v, i, j) -> (j == 2), ["format" => "d mmmm yyyy"])
                ExcelFormatter((v, i, j) -> (j == 3), ["format" => "hh:mm"])
                ExcelFormatter(
                    (v, i, j) -> (j == 4), ["format" => "yyyy-mm-dd\"T\"hh:mm:ss"]
                )
            ],
            data_column_widths = [12.0, 16.0, 8.0, 20.0],
        )

        @test XLSX.getFormat(result[1], "A2").format["numFmt"]["formatCode"] == "m/d/yyyy"
        @test XLSX.getFormat(result[1], "B2").format["numFmt"]["formatCode"] ==
            "d mmmm yyyy"
        @test XLSX.getFormat(result[1], "C2").format["numFmt"]["formatCode"] == "hh:mm"
        @test XLSX.getFormat(result[1], "D2").format["numFmt"]["formatCode"] ==
            "yyyy-mm-dd\"T\"hh:mm:ss"
    end
end
