use clap::CommandFactory;
use forge::cmd::coverage::CoverageArgs;
use foundry_common::fs::{self, files_with_ext};
use foundry_test_utils::{
    TestCommand, TestProject,
    snapbox::{Data, IntoData},
    util::OutputExt,
};
use serde_json::Value;
use std::path::Path;

#[track_caller]
fn assert_lcov(cmd: &mut TestCommand, data: impl IntoData) {
    cmd.args(["--report=lcov", "--report-file"]).assert_file(data.into_data());
}

fn basic_base(prj: TestProject, mut cmd: TestCommand) {
    cmd.args(["coverage", "--report=lcov", "--report=summary"]).assert_success().stdout_eq(str![[
        r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!
Analysing contracts...
Running tests...

Ran 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, [AVG_GAS])
[PASS] test_Increment() ([GAS])
Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests)
Wrote LCOV report.

╭----------------------+---------------+---------------+------------+---------------╮
| File                 | % Lines       | % Statements  | % Branches | % Funcs       |
+===================================================================================+
| script/Counter.s.sol | 0.00% (0/5)   | 0.00% (0/3)   | N/A (0/0)  | 0.00% (0/2)   |
|----------------------+---------------+---------------+------------+---------------|
| src/Counter.sol      | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
|----------------------+---------------+---------------+------------+---------------|
| Total                | 44.44% (4/9)  | 40.00% (2/5)  | N/A (0/0)  | 50.00% (2/4)  |
╰----------------------+---------------+---------------+------------+---------------╯

"#
    ]]);

    let lcov = prj.root().join("lcov.info");
    assert!(lcov.exists(), "lcov.info was not created");
    let default_lcov = str![[r#"
TN:
SF:script/Counter.s.sol
DA:10,0
FN:10,CounterScript.setUp
FNDA:0,CounterScript.setUp
DA:12,0
FN:12,CounterScript.run
FNDA:0,CounterScript.run
DA:13,0
DA:15,0
DA:17,0
FNF:2
FNH:0
LF:5
LH:0
BRF:0
BRH:0
end_of_record
TN:
SF:src/Counter.sol
DA:7,258
FN:7,Counter.setNumber
FNDA:258,Counter.setNumber
DA:8,258
DA:11,1
FN:11,Counter.increment
FNDA:1,Counter.increment
DA:12,1
FNF:2
FNH:2
LF:4
LH:4
BRF:0
BRH:0
end_of_record

"#]];
    assert_data_eq!(Data::read_from(&lcov, None), default_lcov.clone());
    assert_lcov(
        cmd.forge_fuse().args(["coverage", "--report=lcov", "--lcov-version=1"]),
        default_lcov,
    );

    assert_lcov(
        cmd.forge_fuse().args(["coverage", "--report=lcov", "--lcov-version=2"]),
        str![[r#"
TN:
SF:script/Counter.s.sol
DA:10,0
FN:10,10,CounterScript.setUp
FNDA:0,CounterScript.setUp
DA:12,0
FN:12,18,CounterScript.run
FNDA:0,CounterScript.run
DA:13,0
DA:15,0
DA:17,0
FNF:2
FNH:0
LF:5
LH:0
BRF:0
BRH:0
end_of_record
TN:
SF:src/Counter.sol
DA:7,258
FN:7,9,Counter.setNumber
FNDA:258,Counter.setNumber
DA:8,258
DA:11,1
FN:11,13,Counter.increment
FNDA:1,Counter.increment
DA:12,1
FNF:2
FNH:2
LF:4
LH:4
BRF:0
BRH:0
end_of_record

"#]],
    );

    assert_lcov(
        cmd.forge_fuse().args(["coverage", "--report=lcov", "--lcov-version=2.2"]),
        str![[r#"
TN:
SF:script/Counter.s.sol
DA:10,0
FNL:0,10,10
FNA:0,0,CounterScript.setUp
DA:12,0
FNL:1,12,18
FNA:1,0,CounterScript.run
DA:13,0
DA:15,0
DA:17,0
FNF:2
FNH:0
LF:5
LH:0
BRF:0
BRH:0
end_of_record
TN:
SF:src/Counter.sol
DA:7,258
FNL:2,7,9
FNA:2,258,Counter.setNumber
DA:8,258
DA:11,1
FNL:3,11,13
FNA:3,1,Counter.increment
DA:12,1
FNF:2
FNH:2
LF:4
LH:4
BRF:0
BRH:0
end_of_record

"#]],
    );
}

forgetest_init!(basic, |prj, cmd| {
    prj.initialize_default_contracts();
    basic_base(prj, cmd);
});

forgetest_init!(basic_crlf, |prj, cmd| {
    prj.initialize_default_contracts();
    // Manually replace `\n` with `\r\n` in the source file.
    let make_crlf = |path: &Path| {
        fs::write(path, fs::read_to_string(path).unwrap().replace('\n', "\r\n")).unwrap()
    };
    make_crlf(&prj.paths().sources.join("Counter.sol"));
    make_crlf(&prj.paths().scripts.join("Counter.s.sol"));

    // Should have identical stdout and lcov output.
    basic_base(prj, cmd);
});

forgetest!(setup, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    int public i;

    function init() public {
        i = 0;
    }

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a;

    function setUp() public {
        a = new AContract();
        a.init();
    }

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    // Assert 100% coverage (init function coverage called in setUp is accounted).
    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+------------+---------------╮
| File              | % Lines       | % Statements  | % Branches | % Funcs       |
+================================================================================+
| src/AContract.sol | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
|-------------------+---------------+---------------+------------+---------------|
| Total             | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
╰-------------------+---------------+---------------+------------+---------------╯

"#]]);
});

forgetest!(setup_md, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    int public i;

    function init() public {
        i = 0;
    }

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a;

    function setUp() public {
        a = new AContract();
        a.init();
    }

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    // Assert 100% coverage (init function coverage called in setUp is accounted).
    cmd.arg("coverage").args(["--md"]).assert_success().stdout_eq(str![[r#"
...
| File              | % Lines       | % Statements  | % Branches | % Funcs       |
|-------------------|---------------|---------------|------------|---------------|
| src/AContract.sol | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
| Total             | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |

"#]]);
});

forgetest!(no_match, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    int public i;

    function init() public {
        i = 0;
    }

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a;

    function setUp() public {
        a = new AContract();
        a.init();
    }

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    prj.add_source(
        "BContract.sol",
        r#"
contract BContract {
    int public i;

    function init() public {
        i = 0;
    }

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "BContractTest.sol",
        r#"
import "./test.sol";
import {BContract} from "./BContract.sol";

contract BContractTest is DSTest {
    BContract a;

    function setUp() public {
        a = new BContract();
        a.init();
    }

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    // Assert AContract is not included in report.
    cmd.arg("coverage").arg("--no-match-coverage=AContract").assert_success().stdout_eq(str![[
        r#"
...
╭-------------------+---------------+---------------+------------+---------------╮
| File              | % Lines       | % Statements  | % Branches | % Funcs       |
+================================================================================+
| src/BContract.sol | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
|-------------------+---------------+---------------+------------+---------------|
| Total             | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
╰-------------------+---------------+---------------+------------+---------------╯

"#
    ]]);
});

// `[profile.default.coverage] skip_files` should exclude matching sources from
// the coverage report just like `--no-match-coverage`, but using glob patterns
// from `foundry.toml`.
forgetest!(skip_files_via_config, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    int public i;

    function init() public {
        i = 0;
    }

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a;

    function setUp() public {
        a = new AContract();
        a.init();
    }

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    prj.add_source(
        "BContract.sol",
        r#"
contract BContract {
    int public i;

    function init() public {
        i = 0;
    }

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "BContractTest.sol",
        r#"
import "./test.sol";
import {BContract} from "./BContract.sol";

contract BContractTest is DSTest {
    BContract a;

    function setUp() public {
        a = new BContract();
        a.init();
    }

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    prj.update_config(|config| {
        config.coverage.skip_files =
            vec!["src/AContract.sol".into(), "src/AContractTest.sol".into()];
    });

    // Assert AContract.sol is excluded via the config glob; BContract.sol still
    // appears in the report.
    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+------------+---------------╮
| File              | % Lines       | % Statements  | % Branches | % Funcs       |
+================================================================================+
| src/BContract.sol | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
|-------------------+---------------+---------------+------------+---------------|
| Total             | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
╰-------------------+---------------+---------------+------------+---------------╯

"#]]);
});

forgetest!(assert, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    function checkA(uint256 a) external pure returns (bool) {
        assert(a > 2);
        return true;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

interface Vm {
    function expectRevert() external;
}

contract AContractTest is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);
    AContract a = new AContract();

    function testAssertBranch() external {
        bool result = a.checkA(10);
        assertTrue(result);
    }

    function testAssertRevertBranch() external {
        vm.expectRevert();
        a.checkA(1);
    }
}
    "#,
    );

    // Assert 50% statement coverage for assert failure (assert not considered a branch).
    cmd.arg("coverage").args(["--mt", "testAssertRevertBranch"]).assert_success().stdout_eq(str![
        [r#"
...
╭-------------------+--------------+--------------+------------+---------------╮
| File              | % Lines      | % Statements | % Branches | % Funcs       |
+==============================================================================+
| src/AContract.sol | 66.67% (2/3) | 50.00% (1/2) | N/A (0/0)  | 100.00% (1/1) |
|-------------------+--------------+--------------+------------+---------------|
| Total             | 66.67% (2/3) | 50.00% (1/2) | N/A (0/0)  | 100.00% (1/1) |
╰-------------------+--------------+--------------+------------+---------------╯

"#]
    ]);

    // Assert 100% statement coverage for proper assert (assert not considered a branch).
    cmd.forge_fuse().arg("coverage").args(["--mt", "testAssertBranch"]).assert_success().stdout_eq(
        str![[r#"
...
╭-------------------+---------------+---------------+------------+---------------╮
| File              | % Lines       | % Statements  | % Branches | % Funcs       |
+================================================================================+
| src/AContract.sol | 100.00% (3/3) | 100.00% (2/2) | N/A (0/0)  | 100.00% (1/1) |
|-------------------+---------------+---------------+------------+---------------|
| Total             | 100.00% (3/3) | 100.00% (2/2) | N/A (0/0)  | 100.00% (1/1) |
╰-------------------+---------------+---------------+------------+---------------╯

"#]],
    );
});

forgetest!(require, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    function checkRequire(bool doNotRevert) public view {
        require(doNotRevert, "reverted");
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

interface Vm {
    function expectRevert(bytes calldata revertData) external;
}

contract AContractTest is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);
    AContract a = new AContract();

    function testRequireRevert() external {
        vm.expectRevert(abi.encodePacked("reverted"));
        a.checkRequire(false);
    }

    function testRequireNoRevert() external {
        a.checkRequire(true);
    }
}
    "#,
    );

    // Assert 50% branch coverage if only revert tested.
    cmd.arg("coverage").args(["--mt", "testRequireRevert"]).assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+--------------+---------------╮
| File              | % Lines       | % Statements  | % Branches   | % Funcs       |
+==================================================================================+
| src/AContract.sol | 100.00% (2/2) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) |
|-------------------+---------------+---------------+--------------+---------------|
| Total             | 100.00% (2/2) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) |
╰-------------------+---------------+---------------+--------------+---------------╯

"#]]);

    // Assert 50% branch coverage if only happy path tested.
    cmd.forge_fuse()
        .arg("coverage")
        .args(["--mt", "testRequireNoRevert"])
        .assert_success()
        .stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+--------------+---------------╮
| File              | % Lines       | % Statements  | % Branches   | % Funcs       |
+==================================================================================+
| src/AContract.sol | 100.00% (2/2) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) |
|-------------------+---------------+---------------+--------------+---------------|
| Total             | 100.00% (2/2) | 100.00% (1/1) | 50.00% (1/2) | 100.00% (1/1) |
╰-------------------+---------------+---------------+--------------+---------------╯

"#]]);

    // Assert 100% branch coverage.
    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+---------------+---------------╮
| File              | % Lines       | % Statements  | % Branches    | % Funcs       |
+===================================================================================+
| src/AContract.sol | 100.00% (2/2) | 100.00% (1/1) | 100.00% (2/2) | 100.00% (1/1) |
|-------------------+---------------+---------------+---------------+---------------|
| Total             | 100.00% (2/2) | 100.00% (1/1) | 100.00% (2/2) | 100.00% (1/1) |
╰-------------------+---------------+---------------+---------------+---------------╯

"#]]);
});

forgetest!(line_hit_not_doubled, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    int public i;

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a = new AContract();

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    // We want to make sure DA:8,1 is added only once so line hit is not doubled.
    assert_lcov(
        cmd.arg("coverage"),
        str![[r#"
TN:
SF:src/AContract.sol
DA:7,1
FN:7,AContract.foo
FNDA:1,AContract.foo
DA:8,1
FNF:1
FNH:1
LF:2
LH:2
BRF:0
BRH:0
end_of_record

"#]],
    );
});

forgetest!(branch, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Foo.sol",
        r#"
contract Foo {
    error Gte1(uint256 number, uint256 firstElement);

    enum Status {
        NULL,
        OPEN,
        CLOSED
    }

    struct Item {
        Status status;
        uint256 value;
    }

    mapping(uint256 => Item) internal items;
    uint256 public nextId = 1;

    function getItem(uint256 id) public view returns (Item memory item) {
        item = items[id];
    }

    function addItem(uint256 value) public returns (uint256 id) {
        id = nextId;
        items[id] = Item(Status.OPEN, value);
        nextId++;
    }

    function closeIfEqValue(uint256 id, uint256 value) public {
        if (items[id].value == value) {
            items[id].status = Status.CLOSED;
        }
    }

    function incrementIfEqValue(uint256 id, uint256 value) public {
        if (items[id].value == value) {
            items[id].value = value + 1;
        }
    }

    function foo(uint256 a) external pure {
        if (a < 10) {
            if (a < 3) {
                assert(a == 1);
            } else {
                assert(a == 5);
            }
        } else {
            assert(a == 60);
        }
    }

    function countOdd(uint256[] memory arr) external pure returns (uint256 count) {
        uint256 length = arr.length;
        for (uint256 i = 0; i < length; ++i) {
            if (arr[i] % 2 == 1) {
                count++;
                arr[0];
            }
        }
    }

    function checkLt(uint256 number, uint256[] memory arr) external pure returns (bool) {
        if (number >= arr[0]) {
            revert Gte1(number, arr[0]);
        }
        return true;
    }

    function checkEmptyStatements(uint256 number, uint256[] memory arr) external pure returns (bool) {
        // Check that empty statements are covered.
        if (number >= arr[0]) {
            // Do nothing
        } else {
            // Do nothing.
        }
        if (number >= arr[0]) {}

        return true;
    }

    function singlePathCoverage(uint256 number) external pure {
        if (number < 10) {
            if (number < 5) {
                number++;
            }
            number++;
        }
    }
}
    "#,
    );

    prj.add_source(
        "FooTest.sol",
        r#"
import "./test.sol";
import {Foo} from "./Foo.sol";

interface Vm {
    function expectRevert(bytes calldata revertData) external;
    function expectRevert() external;
}

contract FooTest is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);
    Foo internal foo = new Foo();

    function test_issue_7784() external {
        foo.foo(1);
        vm.expectRevert();
        foo.foo(2);
        vm.expectRevert();
        foo.foo(4);
        foo.foo(5);
        foo.foo(60);
        vm.expectRevert();
        foo.foo(70);
    }

    function test_issue_4310() external {
        uint256[] memory arr = new uint256[](3);
        arr[0] = 78;
        arr[1] = 493;
        arr[2] = 700;
        uint256 count = foo.countOdd(arr);
        assertEq(count, 1);

        arr = new uint256[](4);
        arr[0] = 78;
        arr[1] = 493;
        arr[2] = 700;
        arr[3] = 1729;
        count = foo.countOdd(arr);
        assertEq(count, 2);
    }

    function test_issue_4315() external {
        uint256 value = 42;
        uint256 id = foo.addItem(value);
        assertEq(id, 1);
        assertEq(foo.nextId(), 2);
        Foo.Item memory item = foo.getItem(id);
        assertEq(uint8(item.status), uint8(Foo.Status.OPEN));
        assertEq(item.value, value);

        foo = new Foo();
        id = foo.addItem(value);
        foo.closeIfEqValue(id, 903);
        item = foo.getItem(id);
        assertEq(uint8(item.status), uint8(Foo.Status.OPEN));

        foo = new Foo();
        foo.addItem(value);
        foo.closeIfEqValue(id, 42);
        item = foo.getItem(id);
        assertEq(uint8(item.status), uint8(Foo.Status.CLOSED));

        foo = new Foo();
        id = foo.addItem(value);
        foo.incrementIfEqValue(id, 903);
        item = foo.getItem(id);
        assertEq(item.value, 42);

        foo = new Foo();
        id = foo.addItem(value);
        foo.incrementIfEqValue(id, 42);
        item = foo.getItem(id);
        assertEq(item.value, 43);
    }

    function test_issue_4309() external {
        uint256[] memory arr = new uint256[](1);
        arr[0] = 1;
        uint256 number = 2;
        vm.expectRevert(abi.encodeWithSelector(Foo.Gte1.selector, number, arr[0]));
        foo.checkLt(number, arr);

        number = 1;
        vm.expectRevert(abi.encodeWithSelector(Foo.Gte1.selector, number, arr[0]));
        foo.checkLt(number, arr);

        number = 0;
        bool result = foo.checkLt(number, arr);
        assertTrue(result);
    }

    function test_issue_4314() external {
        uint256[] memory arr = new uint256[](1);
        arr[0] = 1;
        foo.checkEmptyStatements(0, arr);
    }

    function test_single_path_child_branch() external {
        foo.singlePathCoverage(1);
    }

    function test_single_path_parent_branch() external {
        foo.singlePathCoverage(9);
    }

    function test_single_path_branch() external {
        foo.singlePathCoverage(15);
    }
}
    "#,
    );

    // Assert no coverage for single path branch. 2 branches (parent and child) not covered.
    cmd.arg("coverage")
        .args(["--nmt", "test_single_path_child_branch|test_single_path_parent_branch"])
        .assert_success()
        .stdout_eq(str![[r#"
...
╭-------------+----------------+----------------+---------------+---------------╮
| File        | % Lines        | % Statements   | % Branches    | % Funcs       |
+===============================================================================+
| src/Foo.sol | 91.67% (33/36) | 90.00% (27/30) | 80.00% (8/10) | 100.00% (9/9) |
|-------------+----------------+----------------+---------------+---------------|
| Total       | 91.67% (33/36) | 90.00% (27/30) | 80.00% (8/10) | 100.00% (9/9) |
╰-------------+----------------+----------------+---------------+---------------╯

"#]]);

    // Assert no coverage for single path child branch. 1 branch (child) not covered.
    cmd.forge_fuse()
        .arg("coverage")
        .args(["--nmt", "test_single_path_child_branch"])
        .assert_success()
        .stdout_eq(str![[r#"
...
╭-------------+----------------+----------------+---------------+---------------╮
| File        | % Lines        | % Statements   | % Branches    | % Funcs       |
+===============================================================================+
| src/Foo.sol | 97.22% (35/36) | 96.67% (29/30) | 90.00% (9/10) | 100.00% (9/9) |
|-------------+----------------+----------------+---------------+---------------|
| Total       | 97.22% (35/36) | 96.67% (29/30) | 90.00% (9/10) | 100.00% (9/9) |
╰-------------+----------------+----------------+---------------+---------------╯

"#]]);

    // Assert 100% coverage.
    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------+-----------------+-----------------+-----------------+---------------╮
| File        | % Lines         | % Statements    | % Branches      | % Funcs       |
+===================================================================================+
| src/Foo.sol | 100.00% (36/36) | 100.00% (30/30) | 100.00% (10/10) | 100.00% (9/9) |
|-------------+-----------------+-----------------+-----------------+---------------|
| Total       | 100.00% (36/36) | 100.00% (30/30) | 100.00% (10/10) | 100.00% (9/9) |
╰-------------+-----------------+-----------------+-----------------+---------------╯

"#]]);
});

forgetest!(function_call, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    struct Custom {
        bool a;
        uint256 b;
    }

    function coverMe() external returns (bool) {
        // Next lines should not be counted in coverage.
        string("");
        uint256(1);
        address(this);
        bool(false);
        Custom(true, 10);
        // Next lines should be counted in coverage.
        uint256 a = uint256(1);
        Custom memory cust = Custom(false, 100);
        privateWithNoBody();
        privateWithBody();
        publicWithNoBody();
        publicWithBody();
        return true;
    }

    function privateWithNoBody() private {}

    function privateWithBody() private returns (bool) {
        return true;
    }

    function publicWithNoBody() private {}

    function publicWithBody() private returns (bool) {
        return true;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a = new AContract();

    function testTypeConversionCoverage() external {
        a.coverMe();
    }
}
    "#,
    );

    // Assert 100% coverage.
    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+-----------------+---------------+------------+---------------╮
| File              | % Lines         | % Statements  | % Branches | % Funcs       |
+==================================================================================+
| src/AContract.sol | 100.00% (14/14) | 100.00% (9/9) | N/A (0/0)  | 100.00% (5/5) |
|-------------------+-----------------+---------------+------------+---------------|
| Total             | 100.00% (14/14) | 100.00% (9/9) | N/A (0/0)  | 100.00% (5/5) |
╰-------------------+-----------------+---------------+------------+---------------╯

"#]]);
});

forgetest!(try_catch, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Foo.sol",
        r#"
contract Foo {
    address public owner;

    constructor(address _owner) {
        require(_owner != address(0), "invalid address");
        assert(_owner != 0x0000000000000000000000000000000000000001);
        owner = _owner;
    }

    function myFunc(uint256 x) public pure returns (string memory) {
        require(x != 0, "require failed");
        return "my func was called";
    }
}

contract Bar {
    event Log(string message);
    event LogBytes(bytes data);

    Foo public foo;

    constructor() {
        foo = new Foo(msg.sender);
    }

    function tryCatchExternalCall(uint256 _i) public {
        try foo.myFunc(_i) returns (string memory result) {
            emit Log(result);
        } catch {
            emit Log("external call failed");
        }
    }

    function tryCatchNewContract(address _owner) public {
        try new Foo(_owner) returns (Foo foo_) {
            emit Log("Foo created");
        } catch Error(string memory reason) {
            emit Log(reason);
        } catch (bytes memory reason) {}
    }

    function tryCatchAllNewContract(address _owner) public {
        try new Foo(_owner) returns (Foo foo_) {} catch {}
    }
}
    "#,
    );

    prj.add_source(
        "FooTest.sol",
        r#"
import "./test.sol";
import {Bar, Foo} from "./Foo.sol";

interface Vm {
    function expectRevert() external;
}

contract FooTest is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);

    function test_happy_foo_coverage() external {
        vm.expectRevert();
        Foo foo = new Foo(address(0));
        vm.expectRevert();
        foo = new Foo(address(1));
        foo = new Foo(address(2));
    }

    function test_happy_path_coverage() external {
        Bar bar = new Bar();
        bar.tryCatchNewContract(0x0000000000000000000000000000000000000002);
        bar.tryCatchAllNewContract(0x0000000000000000000000000000000000000002);
        bar.tryCatchExternalCall(1);
    }

    function test_coverage() external {
        Bar bar = new Bar();
        bar.tryCatchNewContract(0x0000000000000000000000000000000000000000);
        bar.tryCatchNewContract(0x0000000000000000000000000000000000000001);
        bar.tryCatchAllNewContract(0x0000000000000000000000000000000000000001);
        bar.tryCatchExternalCall(0);
    }
}
    "#,
    );

    // Assert coverage not 100% for happy paths only.
    cmd.arg("coverage").args(["--mt", "happy"]).assert_success().stdout_eq(str![[r#"
...
╭-------------+----------------+----------------+--------------+---------------╮
| File        | % Lines        | % Statements   | % Branches   | % Funcs       |
+==============================================================================+
| src/Foo.sol | 77.27% (17/22) | 78.57% (11/14) | 66.67% (6/9) | 100.00% (6/6) |
|-------------+----------------+----------------+--------------+---------------|
| Total       | 77.27% (17/22) | 78.57% (11/14) | 66.67% (6/9) | 100.00% (6/6) |
╰-------------+----------------+----------------+--------------+---------------╯

"#]]);

    // Assert 100% branch coverage (including clauses without body).
    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------+-----------------+-----------------+---------------+---------------╮
| File        | % Lines         | % Statements    | % Branches    | % Funcs       |
+=================================================================================+
| src/Foo.sol | 100.00% (22/22) | 100.00% (14/14) | 100.00% (9/9) | 100.00% (6/6) |
|-------------+-----------------+-----------------+---------------+---------------|
| Total       | 100.00% (22/22) | 100.00% (14/14) | 100.00% (9/9) | 100.00% (6/6) |
╰-------------+-----------------+-----------------+---------------+---------------╯

"#]]);
});

forgetest!(yul, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Foo.sol",
        r#"
contract Foo {
    uint256[] dynamicArray;

    function readDynamicArrayLength() public view returns (uint256 length) {
        assembly {
            length := sload(dynamicArray.slot)
        }
    }

    function switchAndIfStatements(uint256 n) public pure {
        uint256 y;
        assembly {
            switch n
            case 0 { y := 0 }
            case 1 { y := 1 }
            default { y := n }

            if y { y := 2 }
        }
    }

    function yulForLoop(uint256 n) public {
        uint256 y;
        assembly {
            for { let i := 0 } lt(i, n) { i := add(i, 1) } { y := add(y, 1) }

            let j := 0
            for {} lt(j, n) { j := add(j, 1) } { j := add(j, 2) }
        }
    }

    function hello() public pure returns (bool, uint256, bytes32) {
        bool x;
        uint256 y;
        bytes32 z;

        assembly {
            x := 1
            y := 0xa
            z := "Hello World!"
        }

        return (x, y, z);
    }

    function inlineFunction() public returns (uint256) {
        uint256 result;
        assembly {
            function sum(a, b) -> c {
                c := add(a, b)
            }

            function multiply(a, b) -> c {
                for { let i := 0 } lt(i, b) { i := add(i, 1) } { c := add(c, a) }
            }

            result := sum(2, 3)
            result := multiply(result, 5)
        }
        return result;
    }
}
    "#,
    );

    prj.add_source(
        "FooTest.sol",
        r#"
import "./test.sol";
import {Foo} from "./Foo.sol";

contract FooTest is DSTest {
    function test_foo_coverage() external {
        Foo foo = new Foo();
        foo.switchAndIfStatements(0);
        foo.switchAndIfStatements(1);
        foo.switchAndIfStatements(2);
        foo.yulForLoop(2);
        foo.hello();
        foo.readDynamicArrayLength();
        foo.inlineFunction();
    }
}
    "#,
    );

    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------+-----------------+-----------------+---------------+---------------╮
| File        | % Lines         | % Statements    | % Branches    | % Funcs       |
+=================================================================================+
| src/Foo.sol | 100.00% (30/30) | 100.00% (40/40) | 100.00% (1/1) | 100.00% (7/7) |
|-------------+-----------------+-----------------+---------------+---------------|
| Total       | 100.00% (30/30) | 100.00% (40/40) | 100.00% (1/1) | 100.00% (7/7) |
╰-------------+-----------------+-----------------+---------------+---------------╯

"#]]);
});

forgetest!(misc, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Foo.sol",
        r#"
struct Custom {
    int256 f1;
}

contract A {
    function f(Custom memory custom) public returns (int256) {
        return custom.f1;
    }
}

contract B {
    uint256 public x;

    constructor(uint256 a) payable {
        x = a;
    }
}

contract C {
    function create() public {
        B b = new B{value: 1}(2);
        b = new B{value: 1}(2);
        b = (new B){value: 1}(2);
    }
}

contract D {
    uint256 index;

    function g() public {
        (uint256 x,, uint256 y) = (7, true, 2);
        (x, y) = (y, x);
        (index,,) = (7, true, 2);
    }
}
    "#,
    );

    prj.add_source(
        "FooTest.sol",
        r#"
import "./test.sol";
import "./Foo.sol";

interface Vm {
    function deal(address account, uint256 newBalance) external;
}

contract FooTest is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);

    function test_member_access_coverage() external {
        A a = new A();
        Custom memory cust = Custom(1);
        a.f(cust);
    }

    function test_new_expression_coverage() external {
        B b = new B(1);
        b.x();
        C c = new C();
        vm.deal(address(c), 100 ether);
        c.create();
    }

    function test_tuple_coverage() external {
        D d = new D();
        d.g();
    }
}
    "#,
    );

    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------+-----------------+---------------+------------+---------------╮
| File        | % Lines         | % Statements  | % Branches | % Funcs       |
+============================================================================+
| src/Foo.sol | 100.00% (12/12) | 100.00% (9/9) | N/A (0/0)  | 100.00% (4/4) |
|-------------+-----------------+---------------+------------+---------------|
| Total       | 100.00% (12/12) | 100.00% (9/9) | N/A (0/0)  | 100.00% (4/4) |
╰-------------+-----------------+---------------+------------+---------------╯

"#]]);
});

// https://github.com/foundry-rs/foundry/issues/8605
forgetest!(single_statement, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    event IsTrue(bool isTrue);
    event IsFalse(bool isFalse);

    function ifElseStatementIgnored(bool flag) external returns (bool) {
        if (flag) emit IsTrue(true);
        else emit IsFalse(false);

        bool flag2;
        if (flag) flag2 = true;
        else flag2 = false;

        if (flag2) return true;
        else return false;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a = new AContract();

    function testTrueCoverage() external {
        a.ifElseStatementIgnored(true);
    }

    function testFalseCoverage() external {
        a.ifElseStatementIgnored(false);
    }
}
    "#,
    );

    // Assert 50% coverage for true branches.
    cmd.arg("coverage").args(["--mt", "testTrueCoverage"]).assert_success().stdout_eq(str![[r#"
...
╭-------------------+--------------+--------------+--------------+---------------╮
| File              | % Lines      | % Statements | % Branches   | % Funcs       |
+================================================================================+
| src/AContract.sol | 62.50% (5/8) | 57.14% (4/7) | 50.00% (3/6) | 100.00% (1/1) |
|-------------------+--------------+--------------+--------------+---------------|
| Total             | 62.50% (5/8) | 57.14% (4/7) | 50.00% (3/6) | 100.00% (1/1) |
╰-------------------+--------------+--------------+--------------+---------------╯

"#]]);

    // Assert 50% coverage for false branches.
    cmd.forge_fuse()
        .arg("coverage")
        .args(["--mt", "testFalseCoverage"])
        .assert_success()
        .stdout_eq(str![[r#"
...
╭-------------------+--------------+--------------+--------------+---------------╮
| File              | % Lines      | % Statements | % Branches   | % Funcs       |
+================================================================================+
| src/AContract.sol | 62.50% (5/8) | 57.14% (4/7) | 50.00% (3/6) | 100.00% (1/1) |
|-------------------+--------------+--------------+--------------+---------------|
| Total             | 62.50% (5/8) | 57.14% (4/7) | 50.00% (3/6) | 100.00% (1/1) |
╰-------------------+--------------+--------------+--------------+---------------╯

"#]]);

    // Assert 100% coverage (true/false branches properly covered).
    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+---------------+---------------╮
| File              | % Lines       | % Statements  | % Branches    | % Funcs       |
+===================================================================================+
| src/AContract.sol | 100.00% (8/8) | 100.00% (7/7) | 100.00% (6/6) | 100.00% (1/1) |
|-------------------+---------------+---------------+---------------+---------------|
| Total             | 100.00% (8/8) | 100.00% (7/7) | 100.00% (6/6) | 100.00% (1/1) |
╰-------------------+---------------+---------------+---------------+---------------╯

"#]]);
});

forgetest!(single_statement_loop, |prj, cmd| {
    // TODO(dani): the specific case of `if (x) continue/break` is not properly covered.
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    function ifBreakContinueIgnored(bool flag) external returns (uint256 sum) {
        for (uint256 i = 0; i < 5; i++) {
            if (flag) continue;
            sum += i;
        }

        for (uint256 i = 0; i < 5; i++) {
            if (flag) break;
            sum += i;
        }
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a = new AContract();

    function testTrueCoverage() external {
        a.ifBreakContinueIgnored(true);
    }

    function testFalseCoverage() external {
        a.ifBreakContinueIgnored(false);
    }
}
    "#,
    );

    // Assert 50% coverage for true branches.
    cmd.arg("coverage").args(["--mt", "testTrueCoverage"]).assert_success().stdout_eq(str![[r#"
...
╭-------------------+--------------+---------------+---------------+---------------╮
| File              | % Lines      | % Statements  | % Branches    | % Funcs       |
+==================================================================================+
| src/AContract.sol | 71.43% (5/7) | 70.00% (7/10) | 100.00% (2/2) | 100.00% (1/1) |
|-------------------+--------------+---------------+---------------+---------------|
| Total             | 71.43% (5/7) | 70.00% (7/10) | 100.00% (2/2) | 100.00% (1/1) |
╰-------------------+--------------+---------------+---------------+---------------╯

"#]]);

    // Assert 50% coverage for false branches.
    cmd.forge_fuse()
        .arg("coverage")
        .args(["--mt", "testFalseCoverage"])
        .assert_success()
        .stdout_eq(str![[r#"
...
╭-------------------+---------------+-----------------+---------------+---------------╮
| File              | % Lines       | % Statements    | % Branches    | % Funcs       |
+=====================================================================================+
| src/AContract.sol | 100.00% (7/7) | 100.00% (10/10) | 100.00% (2/2) | 100.00% (1/1) |
|-------------------+---------------+-----------------+---------------+---------------|
| Total             | 100.00% (7/7) | 100.00% (10/10) | 100.00% (2/2) | 100.00% (1/1) |
╰-------------------+---------------+-----------------+---------------+---------------╯

"#]]);

    // Assert 100% coverage (true/false branches properly covered).
    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+-----------------+---------------+---------------╮
| File              | % Lines       | % Statements    | % Branches    | % Funcs       |
+=====================================================================================+
| src/AContract.sol | 100.00% (7/7) | 100.00% (10/10) | 100.00% (2/2) | 100.00% (1/1) |
|-------------------+---------------+-----------------+---------------+---------------|
| Total             | 100.00% (7/7) | 100.00% (10/10) | 100.00% (2/2) | 100.00% (1/1) |
╰-------------------+---------------+-----------------+---------------+---------------╯

"#]]);
});

// https://github.com/foundry-rs/foundry/issues/8604
forgetest!(branch_with_calldata_reads, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    event IsTrue(bool isTrue);
    event IsFalse(bool isFalse);

    function execute(bool[] calldata isTrue) external {
        for (uint256 i = 0; i < isTrue.length; i++) {
            if (isTrue[i]) {
                emit IsTrue(isTrue[i]);
            } else {
                emit IsFalse(!isTrue[i]);
            }
        }
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a = new AContract();

    function testTrueCoverage() external {
        bool[] memory isTrue = new bool[](1);
        isTrue[0] = true;
        a.execute(isTrue);
    }

    function testFalseCoverage() external {
        bool[] memory isFalse = new bool[](1);
        isFalse[0] = false;
        a.execute(isFalse);
    }
}
    "#,
    );

    // Assert 50% coverage for true branches.
    cmd.arg("coverage").args(["--mt", "testTrueCoverage"]).assert_success().stdout_eq(str![[r#"
...
╭-------------------+--------------+--------------+--------------+---------------╮
| File              | % Lines      | % Statements | % Branches   | % Funcs       |
+================================================================================+
| src/AContract.sol | 80.00% (4/5) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) |
|-------------------+--------------+--------------+--------------+---------------|
| Total             | 80.00% (4/5) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) |
╰-------------------+--------------+--------------+--------------+---------------╯

"#]]);

    // Assert 50% coverage for false branches.
    cmd.forge_fuse()
        .arg("coverage")
        .args(["--mt", "testFalseCoverage"])
        .assert_success()
        .stdout_eq(str![[r#"
...
╭-------------------+--------------+--------------+--------------+---------------╮
| File              | % Lines      | % Statements | % Branches   | % Funcs       |
+================================================================================+
| src/AContract.sol | 60.00% (3/5) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) |
|-------------------+--------------+--------------+--------------+---------------|
| Total             | 60.00% (3/5) | 80.00% (4/5) | 50.00% (1/2) | 100.00% (1/1) |
╰-------------------+--------------+--------------+--------------+---------------╯

"#]]);

    // Assert 100% coverage (true/false branches properly covered).
    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+---------------+---------------╮
| File              | % Lines       | % Statements  | % Branches    | % Funcs       |
+===================================================================================+
| src/AContract.sol | 100.00% (5/5) | 100.00% (5/5) | 100.00% (2/2) | 100.00% (1/1) |
|-------------------+---------------+---------------+---------------+---------------|
| Total             | 100.00% (5/5) | 100.00% (5/5) | 100.00% (2/2) | 100.00% (1/1) |
╰-------------------+---------------+---------------+---------------+---------------╯

"#]]);
});

// https://github.com/foundry-rs/foundry/issues/10792
forgetest!(branch_with_storage_bytes_reads, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    bytes public optA = bytes("optA");
    bytes public optB = bytes("optB");

    function execute(uint16 value) external view {
        bytes memory element;
        if (value == 4) {
            element = optA;
        } else {
            element = optB;
        }
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a = new AContract();

    function testTrueCoverage() external view {
        a.execute(4);
    }

    function testFalseCoverage() external view {
        a.execute(5);
    }
}
    "#,
    );

    cmd.arg("coverage").args(["--mt", "testTrueCoverage"]).assert_success().stdout_eq(str![[r#"
...
╭-------------------+--------------+--------------+--------------+---------------╮
| File              | % Lines      | % Statements | % Branches   | % Funcs       |
+================================================================================+
| src/AContract.sol | 80.00% (4/5) | 75.00% (3/4) | 50.00% (1/2) | 100.00% (1/1) |
|-------------------+--------------+--------------+--------------+---------------|
| Total             | 80.00% (4/5) | 75.00% (3/4) | 50.00% (1/2) | 100.00% (1/1) |
╰-------------------+--------------+--------------+--------------+---------------╯

"#]]);

    cmd.forge_fuse()
        .arg("coverage")
        .args(["--mt", "testFalseCoverage"])
        .assert_success()
        .stdout_eq(str![[r#"
...
╭-------------------+--------------+--------------+--------------+---------------╮
| File              | % Lines      | % Statements | % Branches   | % Funcs       |
+================================================================================+
| src/AContract.sol | 80.00% (4/5) | 75.00% (3/4) | 50.00% (1/2) | 100.00% (1/1) |
|-------------------+--------------+--------------+--------------+---------------|
| Total             | 80.00% (4/5) | 75.00% (3/4) | 50.00% (1/2) | 100.00% (1/1) |
╰-------------------+--------------+--------------+--------------+---------------╯

"#]]);

    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+---------------+---------------╮
| File              | % Lines       | % Statements  | % Branches    | % Funcs       |
+===================================================================================+
| src/AContract.sol | 100.00% (5/5) | 100.00% (4/4) | 100.00% (2/2) | 100.00% (1/1) |
|-------------------+---------------+---------------+---------------+---------------|
| Total             | 100.00% (5/5) | 100.00% (4/4) | 100.00% (2/2) | 100.00% (1/1) |
╰-------------------+---------------+---------------+---------------+---------------╯

"#]]);
});

forgetest!(branch_with_code_free_else, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    uint256 public value;

    function execute(bool condition) external {
        if (condition) {
            value = 1;
        } else {
            uint256 unused;
        }
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a = new AContract();

    function testCoverage() external {
        a.execute(true);
        a.execute(false);
    }
}
    "#,
    );

    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+--------------+--------------+---------------+---------------╮
| File              | % Lines      | % Statements | % Branches    | % Funcs       |
+=================================================================================+
| src/AContract.sol | 75.00% (3/4) | 50.00% (1/2) | 100.00% (2/2) | 100.00% (1/1) |
|-------------------+--------------+--------------+---------------+---------------|
| Total             | 75.00% (3/4) | 50.00% (1/2) | 100.00% (2/2) | 100.00% (1/1) |
╰-------------------+--------------+--------------+---------------+---------------╯

"#]]);
});

forgetest!(identical_bytecodes, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    uint256 public number;
    address public immutable usdc1;
    address public immutable usdc2;
    address public immutable usdc3;
    address public immutable usdc4;
    address public immutable usdc5;
    address public immutable usdc6;

    constructor() {
        address a = 0x176211869cA2b568f2A7D4EE941E073a821EE1ff;
        usdc1 = a;
        usdc2 = a;
        usdc3 = a;
        usdc4 = a;
        usdc5 = a;
        usdc6 = a;
    }

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract public counter;

    function setUp() public {
        counter = new AContract();
        counter.setNumber(0);
    }

    function test_Increment() public {
        counter.increment();
        assertEq(counter.number(), 1);
    }
}
    "#,
    );

    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+-----------------+---------------+------------+---------------╮
| File              | % Lines         | % Statements  | % Branches | % Funcs       |
+==================================================================================+
| src/AContract.sol | 100.00% (12/12) | 100.00% (9/9) | N/A (0/0)  | 100.00% (3/3) |
|-------------------+-----------------+---------------+------------+---------------|
| Total             | 100.00% (12/12) | 100.00% (9/9) | N/A (0/0)  | 100.00% (3/3) |
╰-------------------+-----------------+---------------+------------+---------------╯

"#]]);
});

forgetest!(constructors, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    bool public active;

    constructor() {
        active = true;
    }
}

contract BContract {
    bool public active;

    constructor() {
        active = true;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import "./AContract.sol";

contract AContractTest is DSTest {
    function test_constructors() public {
        AContract a = new AContract();
        BContract b = new BContract();
    }
}
    "#,
    );

    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+------------+---------------╮
| File              | % Lines       | % Statements  | % Branches | % Funcs       |
+================================================================================+
| src/AContract.sol | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
|-------------------+---------------+---------------+------------+---------------|
| Total             | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
╰-------------------+---------------+---------------+------------+---------------╯

"#]]);
});

// https://github.com/foundry-rs/foundry/issues/9270, https://github.com/foundry-rs/foundry/issues/9444
// Test that special functions with no statements are not counted.
// TODO: We should support this, but for now just ignore them.
// See TODO in `visit_function_definition`: https://github.com/foundry-rs/foundry/issues/9458
forgetest!(empty_functions, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    constructor() {}

    receive() external payable {}

    function increment() public {}
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import "./AContract.sol";

contract AContractTest is DSTest {
    function test_constructors() public {
        AContract a = new AContract();
        a.increment();
        (bool success,) = address(a).call{value: 1}("");
        require(success);
    }
}
    "#,
    );

    assert_lcov(
        cmd.arg("coverage"),
        str![[r#"
TN:
SF:src/AContract.sol
DA:9,1
FN:9,AContract.increment
FNDA:1,AContract.increment
FNF:1
FNH:1
LF:1
LH:1
BRF:0
BRH:0
end_of_record

"#]],
    );

    // Assert there's only one function (`increment`) reported.
    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+--------------+------------+---------------╮
| File              | % Lines       | % Statements | % Branches | % Funcs       |
+===============================================================================+
| src/AContract.sol | 100.00% (1/1) | N/A (0/0)    | N/A (0/0)  | 100.00% (1/1) |
|-------------------+---------------+--------------+------------+---------------|
| Total             | 100.00% (1/1) | N/A (0/0)    | N/A (0/0)  | 100.00% (1/1) |
╰-------------------+---------------+--------------+------------+---------------╯

"#]]);
});

// Test coverage for `receive` functions.
forgetest!(receive, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    uint256 public counter = 0;

    constructor() {
        counter = 1;
    }

    receive() external payable {
        counter = msg.value;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import "./AContract.sol";

contract AContractTest is DSTest {
    AContract a = new AContract();

    function test_constructors() public {
        address(a).call{value: 5}("");
        require(a.counter() == 5);
    }
}
    "#,
    );

    // Assert both constructor and receive functions coverage reported and appear in LCOV.
    assert_lcov(
        cmd.arg("coverage"),
        str![[r#"
TN:
SF:src/AContract.sol
DA:7,1
FN:7,AContract.constructor
FNDA:1,AContract.constructor
DA:8,1
DA:11,1
FN:11,AContract.receive
FNDA:1,AContract.receive
DA:12,1
FNF:2
FNH:2
LF:4
LH:4
BRF:0
BRH:0
end_of_record

"#]],
    );

    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+------------+---------------╮
| File              | % Lines       | % Statements  | % Branches | % Funcs       |
+================================================================================+
| src/AContract.sol | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
|-------------------+---------------+---------------+------------+---------------|
| Total             | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
╰-------------------+---------------+---------------+------------+---------------╯

"#]]);
});

// https://github.com/foundry-rs/foundry/issues/9322
// Test coverage with `--ir-minimum` for solidity < 0.8.5.
forgetest!(ir_minimum_early, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
pragma solidity 0.8.4;

contract AContract {
    function isContract(address account) internal view returns (bool) {
        bytes32 codehash;
        bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
        assembly {
            codehash := extcodehash(account)
        }
        return (codehash != accountHash && codehash != 0x0);
    }
}
    "#,
    );

    // Assert coverage doesn't fail with `Error: Unknown key "inliner"`.
    cmd.arg("coverage").arg("--ir-minimum").assert_success().stdout_eq(str![[r#"
...
╭-------------------+-------------+--------------+------------+-------------╮
| File              | % Lines     | % Statements | % Branches | % Funcs     |
+===========================================================================+
| src/AContract.sol | 0.00% (0/5) | 0.00% (0/4)  | N/A (0/0)  | 0.00% (0/1) |
|-------------------+-------------+--------------+------------+-------------|
| Total             | 0.00% (0/5) | 0.00% (0/4)  | N/A (0/0)  | 0.00% (0/1) |
╰-------------------+-------------+--------------+------------+-------------╯

"#]]);
});

forgetest!(no_artifacts_written, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    int public i;

    function init() public {
        i = 0;
    }

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a;

    function setUp() public {
        a = new AContract();
        a.init();
    }

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    cmd.forge_fuse().arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-------------------+---------------+---------------+------------+---------------╮
| File              | % Lines       | % Statements  | % Branches | % Funcs       |
+================================================================================+
| src/AContract.sol | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
|-------------------+---------------+---------------+------------+---------------|
| Total             | 100.00% (4/4) | 100.00% (2/2) | N/A (0/0)  | 100.00% (2/2) |
╰-------------------+---------------+---------------+------------+---------------╯
...
"#]]);

    // no artifacts are to be written
    let files = files_with_ext(prj.artifacts(), "json").collect::<Vec<_>>();

    assert!(files.is_empty());
});

forgetest!(attribution_report, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    int public i;

    function init() public {
        i = 0;
    }

    function foo() public {
        i = 1;
    }

    function bar() public {
        i = 2;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a;

    function setUp() public {
        a = new AContract();
        a.init();
    }

    function testFoo() public {
        a.foo();
    }

    function testBar() public {
        a.bar();
    }
}
    "#,
    );

    cmd.arg("coverage").args(["--report=attribution"]).assert_success();

    let attribution = prj.root().join("coverage-attribution.json");
    assert!(attribution.exists(), "coverage attribution report was not created");

    let custom_attribution = prj.root().join("custom-coverage-attribution.json");
    cmd.forge_fuse()
        .arg("coverage")
        .args(["--report=attribution", "--report-file", "custom-coverage-attribution.json"])
        .assert_success();
    assert!(custom_attribution.exists(), "custom coverage attribution report was not created");

    let json: Value = serde_json::from_str(&std::fs::read_to_string(attribution).unwrap()).unwrap();
    let tests = json["tests"].as_array().unwrap();
    assert_eq!(tests.len(), 2);

    for test in tests {
        assert_eq!(test["status"], "success");
        assert_eq!(test["kind"], "unit");

        let covered = test["covered"].as_array().unwrap();
        assert!(
            covered.iter().any(|item| item["source"] == "src/AContract.sol"
                && item["kind"] == "function"
                && item["function"] == "init"),
            "setUp coverage should be attributed to each selected test"
        );
    }

    let test_foo = tests.iter().find(|test| test["test"] == "testFoo()").unwrap();
    assert!(test_foo["covered"].as_array().unwrap().iter().any(|item| {
        item["source"] == "src/AContract.sol"
            && item["kind"] == "function"
            && item["function"] == "foo"
    }));

    let test_bar = tests.iter().find(|test| test["test"] == "testBar()").unwrap();
    assert!(test_bar["covered"].as_array().unwrap().iter().any(|item| {
        item["source"] == "src/AContract.sol"
            && item["kind"] == "function"
            && item["function"] == "bar"
    }));
});

forgetest!(attribution_report_keeps_items_aligned_after_filtering_sources, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "sub/WContract.sol",
        r#"
contract WContract {
    function hit() public pure returns (uint256) {
        return 2;
    }
}
    "#,
    );
    prj.add_source(
        "XContract.sol",
        r#"
contract XContract {
    function hit() public pure returns (uint256) {
        return 4;
    }
}
    "#,
    );
    prj.add_source(
        "YContract.sol",
        r#"
contract YContract {
    function hit() public pure returns (uint256) {
        return 6;
    }
}
    "#,
    );
    prj.add_source(
        "ZContract.sol",
        r#"
contract ZContract {
    function hit() public pure returns (uint256) {
        return 8;
    }
}
    "#,
    );

    prj.add_source(
        "AttributionFilterTest.sol",
        r#"
import "./test.sol";
import {WContract} from "./sub/WContract.sol";
import {XContract} from "./XContract.sol";
import {YContract} from "./YContract.sol";
import {ZContract} from "./ZContract.sol";

contract AttributionFilterTest is DSTest {
    function testW() public {
        assertEq(new WContract().hit(), 2);
    }

    function testX() public {
        assertEq(new XContract().hit(), 4);
    }

    function testY() public {
        assertEq(new YContract().hit(), 6);
    }

    function testZ() public {
        assertEq(new ZContract().hit(), 8);
    }
}
    "#,
    );

    cmd.arg("coverage")
        .args(["--report=attribution", "--no-match-coverage=YContract"])
        .assert_success();

    let attribution = prj.root().join("coverage-attribution.json");
    let json: Value = serde_json::from_str(&std::fs::read_to_string(attribution).unwrap()).unwrap();
    let tests = json["tests"].as_array().unwrap();

    assert!(
        tests.iter().flat_map(|test| test["covered"].as_array().unwrap()).all(|item| {
            item["source"].as_str().is_some_and(|source| !source.contains("YContract.sol"))
        }),
        "filtered source should not be present in attribution output"
    );

    for (test_name, contract_name) in
        [("testW()", "WContract"), ("testX()", "XContract"), ("testZ()", "ZContract")]
    {
        let test = tests.iter().find(|test| test["test"] == test_name).unwrap();
        let covered = test["covered"].as_array().unwrap();
        assert!(
            covered.iter().any(|item| {
                item["source"]
                    .as_str()
                    .is_some_and(|source| source.ends_with(&format!("{contract_name}.sol")))
                    && item["contract"] == contract_name
                    && item["kind"] == "function"
                    && item["function"] == "hit"
                    && item["hits"].as_u64().is_some_and(|hits| hits > 0)
            }),
            "{test_name} should retain coverage for {contract_name}.hit"
        );
    }
});

forgetest!(attribution_report_with_lcov_uses_default_paths, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "AContract.sol",
        r#"
contract AContract {
    int public i;

    function foo() public {
        i = 1;
    }
}
    "#,
    );

    prj.add_source(
        "AContractTest.sol",
        r#"
import "./test.sol";
import {AContract} from "./AContract.sol";

contract AContractTest is DSTest {
    AContract a;

    function setUp() public {
        a = new AContract();
    }

    function testFoo() public {
        a.foo();
    }
}
    "#,
    );

    let stderr = cmd
        .arg("coverage")
        .args(["--report=attribution", "--report=lcov", "--report-file", "combined.out"])
        .assert_success()
        .get_output()
        .stderr_lossy();

    assert!(
        stderr.contains("`--report-file` is ignored when multiple file reports are requested"),
        "expected warning about ignored --report-file, got:\n{stderr}"
    );

    assert!(prj.root().join("coverage-attribution.json").exists());
    assert!(prj.root().join("lcov.info").exists());
    assert!(!prj.root().join("combined.out").exists());
});

// <https://github.com/foundry-rs/foundry/issues/10172>
forgetest!(constructor_with_args, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "ArrayCondition.sol",
        r#"
contract ArrayCondition {
    uint8 public constant MAX_SIZE = 32;
    error TooLarge();
    error EmptyArray();
    // Storage variable to ensure the constructor does something
    uint256 private _arrayLength;

    constructor(uint256[] memory values) {
        // Check for empty array
        if (values.length == 0) {
            revert EmptyArray();
        }

        if (values.length > MAX_SIZE) {
            revert TooLarge();
        }

        // Store the array length
        _arrayLength = values.length;
    }

    function getArrayLength() external view returns (uint256) {
        return _arrayLength;
    }
}
    "#,
    );

    prj.add_source(
        "ArrayConditionTest.sol",
        r#"
import "./test.sol";
import {ArrayCondition} from "./ArrayCondition.sol";

interface Vm {
    function expectRevert(bytes4 revertData) external;
}

contract ArrayConditionTest is DSTest {
    Vm constant vm = Vm(HEVM_ADDRESS);

    function testValidSize() public {
        uint256[] memory values = new uint256[](10);
        ArrayCondition condition = new ArrayCondition(values);
        assertEq(condition.getArrayLength(), 10);
    }

    // Test with maximum array size (should NOT revert)
    function testMaxSize() public {
        uint256[] memory values = new uint256[](32);
        ArrayCondition condition = new ArrayCondition(values);
        assertEq(condition.getArrayLength(), 32);
    }

    // Test with too large array size (should revert)
    function testTooLarge() public {
        uint256[] memory values = new uint256[](33);
        vm.expectRevert(ArrayCondition.TooLarge.selector);
        new ArrayCondition(values);
    }

    // Test with empty array (should revert)
    function testEmptyArray() public {
        uint256[] memory values = new uint256[](0);
        vm.expectRevert(ArrayCondition.EmptyArray.selector);
        new ArrayCondition(values);
    }
}
    "#,
    );

    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭------------------------+---------------+---------------+---------------+---------------╮
| File                   | % Lines       | % Statements  | % Branches    | % Funcs       |
+========================================================================================+
| src/ArrayCondition.sol | 100.00% (8/8) | 100.00% (6/6) | 100.00% (2/2) | 100.00% (2/2) |
|------------------------+---------------+---------------+---------------+---------------|
| Total                  | 100.00% (8/8) | 100.00% (6/6) | 100.00% (2/2) | 100.00% (2/2) |
╰------------------------+---------------+---------------+---------------+---------------╯
...
"#]]);
});

// https://github.com/foundry-rs/foundry/issues/11432
// Test coverage for linked libraries.
forgetest!(linked_library, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Counter.sol",
        r#"
library LibCounter {
    function increment(uint256 number) external returns (uint256) {
        return number + 1;
    }
}

contract Counter {
    uint256 public number;

    function increment() public {
        number = LibCounter.increment(number);
    }
}
    "#,
    );

    prj.add_source(
        "CounterTest.sol",
        r#"
import "./test.sol";
import {Counter} from "./Counter.sol";

contract CounterTest is DSTest {
    function testIncrement() public {
        Counter counter = new Counter();
        counter.increment();
    }
}
    "#,
    );

    // Assert 100% coverage for linked libraries.
    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-----------------+---------------+---------------+------------+---------------╮
| File            | % Lines       | % Statements  | % Branches | % Funcs       |
+==============================================================================+
| src/Counter.sol | 100.00% (4/4) | 100.00% (3/3) | N/A (0/0)  | 100.00% (2/2) |
|-----------------+---------------+---------------+------------+---------------|
| Total           | 100.00% (4/4) | 100.00% (3/3) | N/A (0/0)  | 100.00% (2/2) |
╰-----------------+---------------+---------------+------------+---------------╯
...
"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/10422>
// Test that line hits are properly recorded in lcov report.
forgetest!(do_while_lcov, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Counter.sol",
        r#"
contract Counter {
    uint256 public number = 21;

    function increment() public {
        uint256 i = 0;
        do {
            number++;
            if (number > 20) {
                number -= 2;
            }
        } while (++i < 10);
    }
}
    "#,
    );

    prj.add_source(
        "Counter.t.sol",
        r#"
import "./test.sol";
import "./Counter.sol";

contract CounterTest is DSTest {
    function test_do_while() public {
        Counter counter = new Counter();
        counter.increment();
    }
}
    "#,
    );

    assert_lcov(
        cmd.arg("coverage"),
        str![[r#"
TN:
SF:src/Counter.sol
DA:7,1
FN:7,Counter.increment
FNDA:1,Counter.increment
DA:8,1
DA:10,10
DA:11,10
BRDA:11,0,0,6
DA:12,6
DA:14,10
FNF:1
FNH:1
LF:6
LH:6
BRF:1
BRH:1
end_of_record

"#]],
    );
});

// <https://github.com/foundry-rs/foundry/issues/11183>
// Test that overridden functions are disambiguated in the LCOV report.
forgetest!(disambiguate_functions, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Counter.sol",
        r#"
contract Counter {
    uint256 public number;

    function increment() public {
        number++;
    }
    function increment(uint256 amount) public {
        number += amount;
    }
}
    "#,
    );

    prj.add_source(
        "Counter.t.sol",
        r#"
import "./test.sol";
import "./Counter.sol";

contract CounterTest is DSTest {
    function test_overridden() public {
        Counter counter = new Counter();
        counter.increment();
        counter.increment(1);
        counter.increment(2);
        counter.increment(3);
        assertEq(counter.number(), 7);
    }
}
    "#,
    );

    assert_lcov(
        cmd.arg("coverage"),
        str![[r#"
TN:
SF:src/Counter.sol
DA:7,1
FN:7,Counter.increment.0
FNDA:1,Counter.increment.0
DA:8,1
DA:10,3
FN:10,Counter.increment.1
FNDA:3,Counter.increment.1
DA:11,3
FNF:2
FNH:2
LF:4
LH:4
BRF:0
BRH:0
end_of_record

"#]],
    );
});

// Test that functions of abstract contracts and interfaces should not count in coverage report.
forgetest!(abstract_contract_and_interface, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Counter.sol",
        r#"
interface ContractIf {
    function setNumber(uint256 newNumber) external;
}

abstract contract AbstractCounter {
    function _setNumber(uint256 newNumber) internal virtual;

    function _incrementNumber(uint256 newNumber) internal virtual returns (uint256 inc) {
        inc = newNumber + 1;
    }
}

contract Counter is AbstractCounter, ContractIf {
    uint256 public number;

    function setNumber(uint256 newNumber) public {
        _setNumber(newNumber);
    }

    function _setNumber(uint256 newNumber) internal override {
        number = _incrementNumber(newNumber);
    }

    function _incrementNumber(uint256 newNumber) internal override returns (uint256 inc) {
        inc = super._incrementNumber(newNumber);
    }
}
    "#,
    );
    prj.add_source(
        "CounterTest.sol",
        r#"
import "./test.sol";
import {Counter} from "./Counter.sol";

contract CounterTest is DSTest {
    function testCounter() public {
        Counter counter = new Counter();
        counter.setNumber(0);
    }
}
    "#,
    );

    // Test there are 4 functions reported:
    // - `setNumber`, `_setNumber` and `_incrementNumber` from `Counter` contract
    // - `_incrementNumber` from `AbstractCounter` (virtual with implementation). `_setNumber` is
    // excluded as it is not implemented.
    cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
...
╭-----------------+---------------+---------------+------------+---------------╮
| File            | % Lines       | % Statements  | % Branches | % Funcs       |
+==============================================================================+
| src/Counter.sol | 100.00% (8/8) | 100.00% (4/4) | N/A (0/0)  | 100.00% (4/4) |
|-----------------+---------------+---------------+------------+---------------|
| Total           | 100.00% (8/8) | 100.00% (4/4) | N/A (0/0)  | 100.00% (4/4) |
╰-----------------+---------------+---------------+------------+---------------╯
...
"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/11548>
// Test BRDA hit values follow LCOV spec: "-" when line never executed, "0" when line hit but
// branch not taken. This ensures `genhtml` consistency.
forgetest!(brda_lcov_consistency, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Counter.sol",
        r#"
contract Counter {
    uint256 public number;

    function setPositive(uint256 newNumber) public {
        if (newNumber > 0) {
            number = newNumber;
        } else {
            number = 1;
        }
    }

    function neverCalled(uint256 x) public {
        if (x > 100) {
            number = x;
        } else {
            number = 100;
        }
    }
}
    "#,
    );

    prj.add_source(
        "Counter.t.sol",
        r#"
import "./test.sol";
import "./Counter.sol";

contract CounterTest is DSTest {
    function test_only_positive_branch() public {
        Counter counter = new Counter();
        counter.setPositive(42);
        counter.setPositive(100);
    }
}
    "#,
    );

    // Verify BRDA values:
    // - BRDA:8,0,0,2 - if branch taken 2 times
    // - BRDA:8,0,1,0 - else branch NOT taken but line was hit (outputs "0", not "-")
    // - BRDA:16,1,0,- - if branch NOT taken AND line never executed (outputs "-")
    // - BRDA:16,1,1,- - else branch NOT taken AND line never executed (outputs "-")
    assert_lcov(
        cmd.arg("coverage"),
        str![[r#"
TN:
SF:src/Counter.sol
DA:7,2
FN:7,Counter.setPositive
FNDA:2,Counter.setPositive
DA:8,2
BRDA:8,0,0,2
BRDA:8,0,1,0
DA:9,2
DA:11,0
DA:15,0
FN:15,Counter.neverCalled
FNDA:0,Counter.neverCalled
DA:16,0
BRDA:16,1,0,-
BRDA:16,1,1,-
DA:17,0
DA:19,0
FNF:2
FNH:1
LF:8
LH:3
BRF:4
BRH:1
end_of_record

"#]],
    );
});

// Test that coverage files are written even when tests fail.
forgetest!(coverage_with_failing_tests, |prj, cmd| {
    prj.insert_ds_test();
    prj.add_source(
        "Counter.sol",
        r#"
contract Counter {
    uint256 public number;

    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }

    function increment() public {
        number++;
    }
}
    "#,
    );

    prj.add_source(
        "CounterTest.sol",
        r#"
import "./test.sol";
import {Counter} from "./Counter.sol";

contract CounterTest is DSTest {
    Counter public counter;

    function setUp() public {
        counter = new Counter();
        counter.setNumber(0);
    }

    function test_Increment() public {
        counter.increment();
        assertEq(counter.number(), 1);
    }

    function test_FailingTest() public {
        counter.increment();
        // This assertion will fail
        assertEq(counter.number(), 999);
    }
}
    "#,
    );

    // Run coverage - this should exit with error code 1 due to failing test,
    // but the lcov file should still be written.
    cmd.arg("coverage").args(["--report=lcov"]).assert_failure();

    // Verify that the lcov.info file was created despite test failure
    let lcov = prj.root().join("lcov.info");
    assert!(lcov.exists(), "lcov.info should be created even when tests fail");

    // Verify the coverage data is valid and includes the counter contract
    let lcov_content = std::fs::read_to_string(&lcov).unwrap();
    assert!(lcov_content.contains("SF:src/Counter.sol"), "Coverage should include Counter.sol");
    assert!(lcov_content.contains("FN:"), "Coverage should include function data");
    assert!(lcov_content.contains("DA:"), "Coverage should include line hit data");
});

forgetest_init!(coverage_rejects_mutation_mode_before_compile, |prj, cmd| {
    prj.add_source(
        "Broken.sol",
        r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Broken {
    function broken() public pure returns (uint256) {
        return;
    }
}
"#,
    );

    let output = cmd.args(["coverage", "--mutate", "src/Broken.sol"]).assert_failure();
    let stderr = output.get_output().stderr_lossy();

    assert!(
        stderr.contains("`--mutate` cannot be combined with: coverage"),
        "unexpected stderr:\n{stderr}"
    );
    assert!(
        !stderr.contains("Compiler run failed"),
        "mutation/coverage conflict should be reported before compile errors:\n{stderr}"
    );
});

forgetest_init!(coverage_rejects_test_only_modes_before_compile, |prj, cmd| {
    prj.add_source(
        "Broken.sol",
        r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Broken {
    function broken() public pure returns (uint256) {
        return;
    }
}
"#,
    );

    for args in [
        &["--json"][..],
        &["--junit"],
        &["--list"],
        &["--debug"],
        &["--flamegraph"],
        &["--flamechart"],
        &["--evm-profile"],
        &["--showmap-out", "showmap"],
        &["--brutalize"],
        &["--replay-symbolic-artifact", "counterexample.json"],
        &["--watch", "--json"],
    ] {
        let output = cmd.forge_fuse().arg("coverage").args(args).assert_failure();
        let stderr = output.get_output().stderr_lossy();

        assert!(
            stderr.contains("`forge coverage` cannot be combined with:"),
            "unexpected stderr for {args:?}:\n{stderr}"
        );
        assert!(
            !stderr.contains("Compiler run failed"),
            "coverage conflict should be reported before compile errors for {args:?}:\n{stderr}"
        );
    }
});

forgetest_init!(coverage_match_path_compiles_selected_tests, |prj, cmd| {
    prj.add_test(
        "Shared.t.sol",
        r#"
abstract contract SharedTest {
    function sharedValue() internal pure returns (uint256) {
        return 1;
    }
}
"#,
    );
    prj.add_test(
        "Selected.t.sol",
        r#"
import {SharedTest} from "./Shared.t.sol";

contract SelectedTest is SharedTest {
    function testSelected() public pure {
        require(sharedValue() == 1);
    }
}
"#,
    );
    prj.add_test(
        "Broken.t.sol",
        r#"
contract BrokenTest {
    function broken() public pure returns (uint256) {
        return;
    }
}
"#,
    );
    prj.add_test(
        "Unselected.t.sol",
        r#"
contract UnselectedTest {
    function testUnselected() public {}
}
"#,
    );
    prj.add_script(
        "Report.s.sol",
        r#"
contract ReportScript {
    function run() public {}
}
"#,
    );

    let output = cmd
        .args(["coverage", "--match-path", "test/Selected.t.sol"])
        .assert_success()
        .get_output()
        .stdout_lossy();
    assert!(output.contains("testSelected()"), "selected test did not run:\n{output}");
    assert!(
        output.contains("script/Report.s.sol"),
        "scripts should remain in path-filtered coverage reports:\n{output}"
    );
    assert!(
        !output.contains("test/Unselected.t.sol"),
        "unselected tests should be omitted from coverage reports:\n{output}"
    );
});

#[test]
fn coverage_help_renders_notes() {
    let help = CoverageArgs::command().render_long_help().to_string();

    assert!(help.contains(concat!(
        "Source attribution:\n",
        "  Coverage follows compiler source maps. Inherited modifier code is reported under"
    )));
    assert!(help.contains("use `--include-libs` to include their coverage."));
    assert!(help.contains(concat!(
        "Compatibility:\n",
        "  `forge coverage` supports test filters and `--watch`, but not test-only output"
    )));
    assert!(!help.contains("\\n"));
}
