struct CSV_Foo
end

struct CSVString
    s::String
end

Base.parse(::Type{CSVString}, s::String) = CSVString(s)
Base.zero(::Type{CSVString}) = CSVString("")

struct Dec64
    x::Float64
end

Base.parse(::Type{Dec64}, s::String) = Dec64(parse(Float64, s))
Base.zero(::Type{Dec64}) = Dec64(0.0)

@testset "CSV.File basics" begin

#test on non-existent file
@test_throws ArgumentError CSV.File("");

#test where datarow > headerrow
@test_throws ArgumentError CSV.File(joinpath(dir, "test_no_header.csv"); datarow=1, header=2);

#test bad types
@test_throws CSV.Error CSV.File(joinpath(dir, "test_float_in_int_column.csv"); types=[Int, Int, Int], strict=true)

# Integer overflow; #100
@test_throws CSV.Error CSV.File(joinpath(dir, "int64_overflow.csv"); types=[Int8], strict=true)

# #172
@test_throws ArgumentError CSV.File(joinpath(dir, "test_newline_line_endings.csv"), types=Dict(1=>Integer))

# #289
tmp = CSV.File(IOBuffer(" \"a, b\", \"c\" "), datarow=1) |> columntable
@test length(tmp) == 2
@test length(tmp[1]) == 1
@test tmp.Column1[1] == "a, b"
@test tmp.Column2[1] == "c"

tmp = CSV.File(IOBuffer(" \"2018-01-01\", \"1\" ,1,2,3"), datarow=1) |> columntable
@test length(tmp) == 5
@test length(tmp[1]) == 1
@test tmp.Column1[1] == Date(2018, 1, 1)
@test tmp.Column2[1] == 1
@test tmp.Column3[1] == 1
@test tmp.Column4[1] == 2
@test tmp.Column5[1] == 3

# #329
f = CSV.File(joinpath(dir, "test_types.csv"), types=Dict(:string=>Union{Missing,DateTime}), silencewarnings=true)
@test f.string[1] === missing

# #352
@test_throws KeyError first(CSV.File(joinpath(dir, "test_types.csv"))).a

@test_throws ArgumentError CSV.File(IOBuffer("a\0b\n1\02\n"); delim='\0')
@test_throws ArgumentError CSV.File(IOBuffer("a\0b\n1\02\n"); delim="\0")

f = CSV.File(IOBuffer("a,b\n1,2\n"); header=3)
@test isempty(f.types)
@test f.rows == 0

f = CSV.File(IOBuffer("a,b\n1,2\n"); datarow=3)
@test f.names == [:a, :b]
@test f.rows == 0

f = CSV.File(IOBuffer("a,b\n1,2\n"); skipto=3)
@test f.names == [:a, :b]
@test f.rows == 0

f = CSV.File(IOBuffer("a,b\n1,2\n"); limit=0)
@test f.names == [:a, :b]
@test f.rows == 0

# delim same as quotechar
f = CSV.File(IOBuffer("a\"b\n1\"2\n"); delim='"')
@test f.rows == 0
@test f.cols == 2
@test f.types == [Missing, Missing]

# delim same as quotechar w/ quoted field
f = CSV.File(IOBuffer("a\"b\n1\"\"2\"\n"); delim='"')
@test f.rows == 0
@test f.cols == 2
@test f.types == [Missing, Missing]

# 387
f = CSV.File(IOBuffer("a,b\n1,1\n1,2\n1,3\n"))

for rows in Iterators.partition(f, 2)
    for row in rows
        # get last property
        getproperty(row, :b)
        @test row.a == 1
    end
end

# 391
rows = collect(f)
@test rows[1].a == 1
@test rows[1].a == 1

# 459
rows = collect(CSV.File(joinpath(dir, "time.csv"); dateformat="H:M:S"))
@test rows[1].time == Time(0)
@test rows[2].time == Time(0, 10)

# 388
f = CSV.File(joinpath(dir, "GSM2230757_human1_umifm_counts.csv"); threaded=false)
@test length(f.names) == 20128
@test length(f) == 3

# 386
f = CSV.File(IOBuffer("fullVisitorId,PredictedLogRevenue\n18966949534117,0\n39738481224681,0\n"), limit=3)
@test f.rows == 2
rows = collect(f)
@test rows[1].fullVisitorId == 18966949534117
@test rows[2].fullVisitorId == 39738481224681

f = CSV.File(IOBuffer("col1,col2\n1.0,hi"), limit=3)
@test (length(f), length(f.names)) == (1, 2)

# 288
f = CSV.File(IOBuffer("x\n\",\"\n\",\""))
@test (length(f), length(f.names)) == (2, 1)

# delimiter auto-detection
f = CSV.File(IOBuffer("x\n1\n2\n"))
@test (length(f), length(f.names)) == (2, 1)

f = CSV.File(IOBuffer("x,y\n1,\n2,\n"))
@test (length(f), length(f.names)) == (2, 2)

f = CSV.File(IOBuffer("x y\n1 \n2 \n"))
@test (length(f), length(f.names)) == (2, 2)

f = CSV.File(IOBuffer("x\ty\n1\t\n2\t\n"))
@test (length(f), length(f.names)) == (2, 2)

f = CSV.File(IOBuffer("x|y\n1|\n2|\n"))
@test (length(f), length(f.names)) == (2, 2)

# type promotion
# int => float
f = CSV.File(IOBuffer("x\n1\n3.14"))
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] === 1.0
@test f.x[2] === 3.14

# int => missing
f = CSV.File(IOBuffer("x\n1\n\n"))
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] == 1
@test f.x[2] === missing

# missing => int
f = CSV.File(IOBuffer("x\n\n1\n"))
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] === missing
@test f.x[2] == 1

# missing => int => float
f = CSV.File(IOBuffer("x\n\n1\n3.14\n"))
@test (length(f), length(f.names)) == (3, 1)
@test f.x[1] === missing
@test f.x[2] === 1.0
@test f.x[3] === 3.14

# int => missing => float
f = CSV.File(IOBuffer("x\n1\n\n3.14\n"))
@test (length(f), length(f.names)) == (3, 1)
@test f.x[1] === 1.0
@test f.x[2] === missing
@test f.x[3] === 3.14

# int => float => missing
f = CSV.File(IOBuffer("x\n1\n3.14\n\n"))
@test (length(f), length(f.names)) == (3, 1)
@test f.x[1] === 1.0
@test f.x[2] === 3.14
@test f.x[3] === missing

# int => string
f = CSV.File(IOBuffer("x\n1\nabc"))
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] == "1"
@test f.x[2] == "abc"

# float => string
f = CSV.File(IOBuffer("x\n3.14\nabc"))
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] == "3.14"
@test f.x[2] == "abc"

# int => float => string
f = CSV.File(IOBuffer("x\n1\n3.14\nabc"))
@test (length(f), length(f.names)) == (3, 1)
@test f.x[1] == "1"
@test f.x[2] == "3.14"
@test f.x[3] == "abc"

# missing => int => float => string
f = CSV.File(IOBuffer("x\n\n1\n3.14\nabc"))
@test (length(f), length(f.names)) == (4, 1)
@test f.x[1] === missing
@test f.x[2] == "1"
@test f.x[3] == "3.14"
@test f.x[4] == "abc"

# missing => catg
f = CSV.File(IOBuffer("x\n\na\n"), pool=true)
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] === missing
@test f.x[2] == "a"

# catg => missing
f = CSV.File(IOBuffer("x\na\n\n"), pool=true)
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] == "a"
@test f.x[2] === missing

# catg => string
f = CSV.File(IOBuffer("x\na\nb\na\nb\na\nb\na\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nn\nm\no\np\nq\nr\n"), pool=0.5)
@test typeof(f.x) == Vector{String}

# a few corner cases for escape strings
f = CSV.File(IOBuffer("\"column name with \"\" escape character inside\"\n1\n"))
@test keys(f[1])[1] == Symbol("column name with \" escape character inside")

f = CSV.File(IOBuffer("\"column name with \"\" escape character inside\",1\n,2"), transpose=true)
@test keys(f[1])[1] == Symbol("column name with \" escape character inside")
@test keys(f[1])[2] == :Column2

f = CSV.File(IOBuffer("x\na\nb\n\"quoted field with \"\" escape character inside\"\n"), pool=true)
@test f.x[1] == "a"
@test f.x[3] == "quoted field with \" escape character inside"

f = CSV.File(IOBuffer("x\na\nb\n\"quoted field with \"\" escape character inside\"\n"), pool=true)
@test f.x[1] == "a"
@test f.x[3] == "quoted field with \" escape character inside"

# invalid quoted field is fatal error
@test_throws CSV.Error CSV.File(IOBuffer("x\n\"quoted field that never ends"))
@test_throws CSV.Error CSV.File(IOBuffer("x\nhey\n\"quoted field that never ends"))
@test_throws CSV.Error CSV.File(IOBuffer("x\n\n\"quoted field that never ends"))
@test_throws CSV.Error CSV.File(IOBuffer("x\n1\n\"quoted field that never ends"))
@test_throws CSV.Error CSV.File(IOBuffer("x\n1.0\n\"quoted field that never ends"))
@test_throws CSV.Error CSV.File(IOBuffer("x\na\n\"quoted field that never ends"), pool=true)

# invalid integer
f = CSV.File(IOBuffer("x\nabc\n"), type=Int)
@test (length(f), length(f.names)) == (1, 1)
@test f.x[1] === missing

@test_throws CSV.Error CSV.File(IOBuffer("x\nabc\n"), type=Int, strict=true)

# transpose corner cases
f = CSV.File(IOBuffer("x,y,1\nx2,y2,2\n"), transpose=true, header=2)
@test f.names == [:y, :y2]
@test (length(f), length(f.names)) == (1, 2)
@test f.y[1] == 1
@test f.y2[1] == 2

f = CSV.File(IOBuffer("x,y,1\nx2,y2,2\n"), transpose=true, header=1, datarow=3)
@test f.names == [:x, :x2]
@test (length(f), length(f.names)) == (1, 2)
@test f.x[1] == 1
@test f.x2[1] == 2

f = CSV.File(IOBuffer("x,y,1\nx2,y2,2\n"), transpose=true, header=false, datarow=3)
@test f.names == [:Column1, :Column2]
@test (length(f), length(f.names)) == (1, 2)
@test f.Column1[1] == 1
@test f.Column2[1] == 2

f = CSV.File(IOBuffer(""), transpose=true, header=false)
@test (length(f), length(f.names)) == (0, 0)

f = CSV.File(IOBuffer(""), transpose=true, header=Symbol[])
@test (length(f), length(f.names)) == (0, 0)

# providing empty header vector
f = CSV.File(IOBuffer("x\nabc\n"), header=Symbol[])
@test f.names == [:Column1]

# Union{Bool, Missing}
f = CSV.File(IOBuffer("x\ntrue\n\n"))
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] === true
@test f.x[2] === missing

# Union{Date, Missing}
f = CSV.File(IOBuffer("x\n2019-01-01\n\n"))
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] === Date(2019, 1, 1)
@test f.x[2] === missing

# use_mmap=false
f = @test_deprecated CSV.File(IOBuffer("x\n2019-01-01\n\n"), use_mmap=false)
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] === Date(2019, 1, 1)
@test f.x[2] === missing

# types is Dict{String, Type}
f = CSV.File(IOBuffer("x\n2019-01-01\n\n"), types=Dict("x"=>Date))
@test (length(f), length(f.names)) == (2, 1)
@test f.x[1] === Date(2019, 1, 1)
@test f.x[2] === missing

# various CSV.File/CSV.Row properties
f = CSV.File(IOBuffer("int,float,date,datetime,bool,null,str,catg,int_float\n1,3.14,2019-01-01,2019-01-01T01:02:03,true,,hey,abc,2\n2,NaN,2019-01-02,2019-01-03T01:02:03,false,,there,abc,3.14\n"), pool=0.3)
@test Tables.istable(f)
@test Tables.rowaccess(typeof(f))
@test Tables.columnaccess(typeof(f))
@test Tables.schema(f) == Tables.Schema([:int, :float, :date, :datetime, :bool, :null, :str, :catg, :int_float], [Int64, Float64, Date, DateTime, Bool, Missing, String, String, Float64])
@test Tables.rows(f) === f
@test eltype(f) <: CSV.Row
row = first(f)
@test propertynames(row) == [:int, :float, :date, :datetime, :bool, :null, :str, :catg, :int_float]
@test row.int == 1
@test row.float == 3.14
@test row.date == Date(2019, 1, 1)
@test row.datetime == DateTime(2019, 1, 1, 1, 2, 3)
@test row.bool === true
@test row.null === missing
@test row.str == "hey"
@test row.catg == "abc"
@test typeof(row.catg) == String
@test row.int_float === 2.0
row = iterate(f, 2)[1]
@test row.int_float === 3.14

# 448
@test_throws ArgumentError CSV.File(IOBuffer("x\n1\n2\n3\n#4"), ignorerepeated=true)

# reported by oxinabox on slack; issue w/ manually specified pool column type and 0 rows
f = CSV.File(IOBuffer("x\n"), types=[CSV.PooledString])
@test (length(f), length(f.names)) == (0, 1)

f = CSV.File(IOBuffer("x\n"), types=[Union{CSV.PooledString, Missing}])
@test (length(f), length(f.names)) == (0, 1)

f = CSV.File(IOBuffer("x\n1\n2\n3\n#4"), comment="#")
@test length(f.x) == 3
@test f.x[end] == 3

# 453
@test_throws ArgumentError CSV.File(IOBuffer("x\n1\n2\n3\n#4"), types=[CSV_Foo])
@test_throws ArgumentError CSV.File(IOBuffer("x\n1\n2\n3\n#4"), types=Dict(:x=>CSV_Foo))

# 447
f = CSV.File(IOBuffer("a,b,c\n1,2,3\n\n"), ignoreemptylines=true)
@test (length(f), length(f.names)) == (1, 3)

f = CSV.File(IOBuffer("zip\n11111-1111\n"), dateformat = "y-m-dTH:M:S.s")
@test (length(f), length(f.names)) == (1, 1)

# Supporting commands across multiple platforms cribbed from julia/test/spawn.jl
catcmd = `cat`
havebb = false
if Sys.iswindows()
    busybox = download("https://frippery.org/files/busybox/busybox.exe", joinpath(tempdir(), "busybox.exe"))
    havebb = try # use busybox-w32 on windows, if available
        success(`$busybox`)
        true
    catch
        false
    end
    if havebb
        catcmd = `$busybox cat`
    end
end
f = @test_deprecated CSV.File(`$(catcmd) $(joinpath(dir, "test_basic.csv"))`)
@test columntable(f) == columntable(CSV.File(joinpath(dir, "test_basic.csv")))

#476
f = CSV.File(transcode(GzipDecompressor, Mmap.mmap(joinpath(dir, "randoms.csv.gz"))))
@test (length(f), length(f.names)) == (70000, 7)

f = CSV.File(IOBuffer("thistime\n10:00:00.0\n12:00:00.0"))
@test typeof(f.thistime[1]) <: Dates.Time
@test f.thistime[1] === Time(10)

# 530
f = CSV.File(IOBuffer(",column2\nNA,2\n2,3"), missingstrings=["NA"])
@test f.names == [:Column1, :column2]

# reported on slack from Kevin Bonham
f = CSV.File(IOBuffer("x\n01:02:03\n\n04:05:06\n"), delim=',')
@test isequal(f.x, [Dates.Time(1,2,3), missing, Dates.Time(4,5,6)])

# 566
f = CSV.File(IOBuffer("x\r\n1\r\n2\r\n3\r\n4\r\n5\r\n"), footerskip=3)
@test length(f) == 2
@test f[1][1] == 1

# 578
f = CSV.File(IOBuffer("h1234567890123456\t"^2262 * "lasthdr\r\n" *"dummy dummy dummy\r\n"* ("1.23\t"^2262 * "2.46\r\n")^10), datarow=3, threaded=false)
@test (length(f), length(f.names)) == (10, 2263)
@test all(x -> eltype(x) == Float64, f.columns)

# Date constructor throws which breaks CSV parsing error handling flow
f = CSV.File(IOBuffer("date\n2020-05-05\n2020-05-32"))

# multiple dateformats
f = CSV.File(IOBuffer("time,date,datetime\n10:00:00.0,04/16/2020,2020-04-16 23:14:00\n"), dateformats=Dict(2=>"mm/dd/yyyy", 3=>"yyyy-mm-dd HH:MM:SS"))
@test length(f) == 1
@test f[1].time == Dates.Time(10)
@test f[1].date == Dates.Date(2020, 4, 16)
@test f[1].datetime == Dates.DateTime(2020, 4, 16, 23, 14)

# custom types
f = CSV.File(
    IOBuffer("int8,uint32,bigint,bigfloat,dec64,csvstring\n1,2,170141183460469231731687303715884105727,3.14,1.02,hey there sailor\n2,,,,,\n");
    types=[Int8, UInt32, BigInt, BigFloat, Dec64, CSVString]
)
@test f.int8 isa Vector{Int8}
@test f.int8 == Int8[1, 2]
@test f.uint32 isa Vector{Union{UInt32, Missing}}
@test isequal(f.uint32, [0x00000002, missing])
@test f.bigint isa CSV.SVec2{BigInt}
@test isequal(f.bigint, [BigInt(170141183460469231731687303715884105727), missing])
@test f.bigfloat isa CSV.SVec2{BigFloat}
@test isequal(f.bigfloat, [parse(BigFloat, "3.14"), missing])
@test f.dec64 isa CSV.SVec{Dec64}
@test isequal(f.dec64, [Dec64(1.02), missing])
@test f.csvstring isa CSV.SVec2{CSVString}
@test isequal(f.csvstring, [CSVString("hey there sailor"), missing])

f = CSV.File(transcode(GzipDecompressor, Mmap.mmap(joinpath(dir, "randoms.csv.gz"))); types=[Int32, CSVString, String, Float64, Dec64, Date, DateTime])
@test f.id isa AbstractVector{Int32}
@test f.first isa AbstractVector{CSVString}
@test f.wage isa AbstractVector{Union{Missing, Dec64}}

f = CSV.File(joinpath(dir, "promotions.csv"); lazystrings=true)
@test eltype.(f.columns) == [Float64, Union{Missing, Int64}, Union{Missing, Float64}, String, Union{Missing, String}, String, String, Union{Missing, Int64}]
@test f.int_string isa CSV.LazyStringVector

f = CSV.File(joinpath(dir, "promotions.csv"); limit=7500, threaded=true)
@test length(f) == 7500

f = CSV.File(IOBuffer("1,2\r\n3,4\r\n\r\n5,6\r\n"); header=["col1", "col2"], ignoreemptylines=true)
@test f.col1 == [1, 3, 5]

f = CSV.File(joinpath(dir, "escape_row_starts.csv"); tasks=2)
@test length(f) == 10000
@test eltype(f.col1) == String
@test eltype(f.col2) == Int64

f = CSV.File(IOBuffer("col1\nhey\nthere\nsailor"); lazystrings=true)
@test f.col1 isa CSV.LazyStringVector
@test Tables.columnnames(f) == [:col1]
@test propertynames(f) == [:col1]
@test occursin("IOBuffer", CSV.getname(f))
@test CSV.getcols(f) == 1
@test Base.IndexStyle(f) == Base.IndexLinear()
@test f.col1 === Tables.getcolumn(f, 1)
@test columntable(f) == columntable(collect(f))
show(f)

f = CSV.File(joinpath(dir, "big_types.csv"); lazystrings=true, pool=false)
@test eltype(f.time) == Dates.Time
@test eltype(f.bool) == Bool
@test f.lazy isa CSV.LazyStringVector
@test eltype(f.lazy) == String
@test eltype(f.lazy_missing) == Union{String, Missing}

r = CSV.Rows(joinpath(dir, "big_types.csv"); lazystrings=false, types=[Dates.Time, Bool, String, Union{String, Missing}])
row = first(r)
@test row.time == Dates.Time(12)
@test row.bool
@test row.lazy == "hey"
@test row.lazy_missing === missing

@test CSV.File(IOBuffer("col1\n1")).col1 == [1]

rows = 0
chunks = CSV.Chunks(joinpath(dir, "promotions.csv"); lazystrings=true)
for chunk in chunks
    rows += length(chunk)
end
@test rows == 10000

# 668
buf = IOBuffer("""
       garbage
       a,b
       1,2
       """)
readline(buf)
f = CSV.File(buf, header=["A", "B"])
@test length(f) == 2
@test f.names == [:A, :B]
@test (f[1].A, f[1].B) == ("a", "b")
@test (f[2].A, f[2].B) == ("1", "2")

# 680: ensure typemap works with custom types
f = CSV.File(IOBuffer("a\n1\n2\n3"); typemap=Dict(Int64=>Int32))
@test f.a isa Vector{Int32}
f = CSV.File(IOBuffer("a\n1\n2\n3"); typemap=Dict(Int64=>String))
@test f.a isa Vector{String}

# 678
f = CSV.File(IOBuffer("""x,y
                                       a,b
                                       a,b
                                       a,b
                                       a,b
                                       a,b
                                       a,b
                                       a,b
                                       a,b
                                       a,b

                                       """))
@test f.x[end] === missing
@test f.y[end] === missing

# 679
f = CSV.File(IOBuffer("a,b,c\n1,2,3\n4,5,6\n"); select=["a"], types=Dict(2=>Int8))
@test f.a == [1, 4]

f = CSV.File(transcode(GzipDecompressor, Mmap.mmap(joinpath(dir, "randoms.csv.gz"))); types=Dict(:id=>Int32), select=["first"])
@test length(f) == 70000
@test eltype(f.first) == String

# 706
f = CSV.File(IOBuffer("a,b\n1,2"); types = Dict{Symbol,Type}(
               :a => CategoricalValue{String,UInt32},
               :b => Union{Missing, CategoricalValue{String,UInt32}},
       ))
@test f.a[1] == "1"
@test f.b[1] == "2"

end
