# frozen_string_literal: true

require 'spec_helper'

RSpec.describe DBF::Table do
  let(:dbf_path) { fixture('dbase_83.dbf') }
  let(:memo_path) { fixture('dbase_83.dbt') }
  let(:table) { DBF::Table.new dbf_path }

  specify 'foxpro versions' do
    expect(DBF::Table::FOXPRO_VERSIONS.keys.sort).to eq %w[30 31 f5 fb].sort
  end

  specify 'row is an alias of record' do
    expect(table.record(1)).to eq table.row(1)
  end

  describe '#initialize' do
    let(:data) { StringIO.new File.read(dbf_path) }
    let(:memo) { StringIO.new File.read(memo_path) }

    describe 'when given a path to an existing dbf file' do
      it 'does not raise an error' do
        expect { DBF::Table.new dbf_path }.to_not raise_error
      end
    end

    describe 'when given a path to a non-existent dbf file' do
      it 'raises a DBF::FileNotFound error' do
        expect { DBF::Table.new 'x' }.to raise_error(DBF::FileNotFoundError, 'file not found: x')
      end
    end

    describe 'when data is nil' do
      it 'raises ArgumentError' do
        expect { DBF::Table.new nil }.to raise_error(ArgumentError, 'data must be a file path or StringIO object')
      end
    end

    describe 'when given paths to existing dbf and memo files' do
      it 'does not raise an error' do
        expect { DBF::Table.new dbf_path, memo_path }.to_not raise_error
      end
    end

    it 'accepts an io-like data object' do
      expect { DBF::Table.new data }.to_not raise_error
    end

    it 'accepts an io-like data and memo object' do
      expect { DBF::Table.new data, memo }.to_not raise_error
    end
  end

  describe '#close' do
    before { table.close }

    it 'closes the io' do
      expect { table.record(1) }.to raise_error(IOError)
    end
  end

  describe '#schema' do
    describe 'when data is IO' do
      let(:control_schema) { File.read(fixture('dbase_83_schema_ar.txt')) }

      it 'matches the test schema fixture' do
        expect(table.schema).to eq control_schema
      end

      it 'raises ArgumentError if there is no matching schema' do
        expect { table.schema(:invalid) }.to raise_error(
          ArgumentError,
          ':invalid is not a valid schema. Valid schemas are: activerecord, json, sequel.'
        )
      end
    end

    describe 'when data is StringIO' do
      let(:data) { StringIO.new File.read(dbf_path) }
      let(:table) { DBF::Table.new data }

      let(:control_schema) { File.read(fixture('dbase_83_schema_ar.txt')) }

      it 'matches the test schema fixture' do
        table.name = 'dbase_83'
        expect(table.schema).to eq control_schema
      end
    end
  end

  describe '#sequel_schema' do
    it 'returns a valid Sequel migration by default' do
      control_schema = File.read(fixture('dbase_83_schema_sq.txt'))
      expect(table.sequel_schema).to eq control_schema
    end

    it 'returns a limited Sequel migration when passed true' do
      control_schema = File.read(fixture('dbase_83_schema_sq_lim.txt'))
      expect(table.sequel_schema).to eq control_schema
    end

  end

  describe '#json_schema' do
    it 'is valid JSON' do
      expect { JSON.parse(table.json_schema) }.to_not raise_error
    end

    it 'matches the test fixture' do
      data = JSON.parse(table.json_schema)
      expect(data).to eq [
        {'name' => 'ID', 'type' => 'N', 'length' => 19, 'decimal' => 0},
        {'name' => 'CATCOUNT', 'type' => 'N', 'length' => 19, 'decimal' => 0},
        {'name' => 'AGRPCOUNT', 'type' => 'N', 'length' => 19, 'decimal' => 0},
        {'name' => 'PGRPCOUNT', 'type' => 'N', 'length' => 19, 'decimal' => 0},
        {'name' => 'ORDER', 'type' => 'N', 'length' => 19, 'decimal' => 0},
        {'name' => 'CODE', 'type' => 'C', 'length' => 50, 'decimal' => 0},
        {'name' => 'NAME', 'type' => 'C', 'length' => 100, 'decimal' => 0},
        {'name' => 'THUMBNAIL', 'type' => 'C', 'length' => 254, 'decimal' => 0},
        {'name' => 'IMAGE', 'type' => 'C', 'length' => 254, 'decimal' => 0},
        {'name' => 'PRICE', 'type' => 'N', 'length' => 13, 'decimal' => 2},
        {'name' => 'COST', 'type' => 'N', 'length' => 13, 'decimal' => 2},
        {'name' => 'DESC', 'type' => 'M', 'length' => 10, 'decimal' => 0},
        {'name' => 'WEIGHT', 'type' => 'N', 'length' => 13, 'decimal' => 2},
        {'name' => 'TAXABLE', 'type' => 'L', 'length' => 1, 'decimal' => 0},
        {'name' => 'ACTIVE', 'type' => 'L', 'length' => 1, 'decimal' => 0}
      ]
    end
  end

  describe '#to_csv' do
    after do
      FileUtils.rm_f 'test.csv'
    end

    describe 'when no path param passed' do
      it 'writes to STDOUT' do
        expect { table.to_csv }.to output.to_stdout
      end
    end

    describe 'when path param passed' do
      before { table.to_csv('test.csv') }

      it 'creates a custom csv file' do
        expect(File).to exist('test.csv')
      end
    end
  end

  describe '#record' do
    it 'return nil for deleted records' do
      allow(table).to receive(:deleted_record?).and_return(true)
      expect(table.record(5)).to be_nil
    end

    describe 'when dbf has no column definitions' do
      let(:dbf_path) { fixture('polygon.dbf') }

      it 'raises a DBF::NoColumnsDefined error' do
        expect { DBF::Table.new(dbf_path).record(1) }.to raise_error(DBF::NoColumnsDefined, 'The DBF file has no columns defined')
      end
    end
  end

  describe '#current_record' do
    it 'returns nil for deleted records' do
      allow(table).to receive(:deleted_record?).and_return(true)
      expect(table.record(0)).to be_nil
    end
  end

  describe '#find' do
    describe 'with index' do
      it 'returns the correct record' do
        expect(table.find(5)).to eq table.record(5)
      end
    end

    describe 'with array of indexes' do
      it 'returns the correct records' do
        expect(table.find([1, 5, 10])).to eq [table.record(1), table.record(5), table.record(10)]
      end
    end

    describe 'with :all' do
      let(:records) do
        table.find(:all, weight: 0.0)
      end

      it 'retrieves only matching records' do
        expect(records.size).to eq 66
      end

      it 'yields to a block if given' do
        record_count = 0
        table.find(:all, weight: 0.0) do |record|
          record_count += 1
          expect(record).to be_a DBF::Record
        end
        expect(record_count).to eq 66
      end

      it 'returns all records if options are empty' do
        expect(table.find(:all)).to eq table.to_a
      end

      it 'returns matching records when used with options' do
        expect(table.find(:all, 'WEIGHT' => 0.0)).to eq(table.select { |r| r['weight'] == 0.0 })
      end

      it 'ANDS multiple search terms' do
        expect(table.find(:all, 'ID' => 30, :IMAGE => 'graphics/00000001/TBC01.jpg')).to be_empty
      end

      it 'matches original column names' do
        expect(table.find(:all, 'WEIGHT' => 0.0)).to_not be_empty
      end

      it 'matches symbolized column names' do
        expect(table.find(:all, WEIGHT: 0.0)).to_not be_empty
      end

      it 'matches downcased column names' do
        expect(table.find(:all, 'weight' => 0.0)).to_not be_empty
      end

      it 'matches symbolized downcased column names' do
        expect(table.find(:all, weight: 0.0)).to_not be_empty
      end
    end

    describe 'with :first' do
      it 'returns the first record if options are empty' do
        expect(table.find(:first)).to eq table.record(0)
      end

      it 'returns the first matching record when used with options' do
        expect(table.find(:first, 'CODE' => 'C')).to eq table.record(5)
      end

      it 'ANDs multiple search terms' do
        expect(table.find(:first, 'ID' => 30, 'IMAGE' => 'graphics/00000001/TBC01.jpg')).to be_nil
      end
    end
  end

  describe '#filename' do
    it 'returns the filename as a string' do
      expect(table.filename).to eq 'dbase_83.dbf'
    end
  end

  describe '#name' do
    describe 'when data is an IO' do
      it 'defaults to the filename less extension' do
        expect(table.name).to eq 'dbase_83'
      end

      it 'is mutable' do
        table.name = 'database_83'
        expect(table.name).to eq 'database_83'
      end
    end

    describe 'when data is a StringIO' do
      let(:data) { StringIO.new File.read(dbf_path) }
      let(:memo) { StringIO.new File.read(memo_path) }
      let(:table) { DBF::Table.new data }

      it 'is nil' do
        expect(table.name).to be_nil
      end

      it 'is mutable' do
        table.name = 'database_83'
        expect(table.name).to eq 'database_83'
      end
    end
  end

  describe '#has_memo_file?' do
    describe 'without a memo file' do
      let(:table) { DBF::Table.new fixture('dbase_03.dbf') }

      it 'is false' do
        expect(table).to_not have_memo_file
      end
    end

    describe 'with a memo file' do
      it 'is true' do
        expect(table).to have_memo_file
      end
    end
  end

  describe '#columns' do
    let(:columns) { table.columns }

    it 'is an array of Columns' do
      expect(columns).to be_an(Array)
      expect(columns).to_not be_empty
      expect(columns).to(be_all { |c| c.is_a? DBF::Column })
    end
  end

  describe '#column_names' do
    let(:column_names) do
      %w[ID CATCOUNT AGRPCOUNT PGRPCOUNT ORDER CODE NAME THUMBNAIL IMAGE PRICE COST DESC WEIGHT TAXABLE ACTIVE]
    end

    describe 'when data is an IO' do
      it 'is an array of all column names' do
        expect(table.column_names).to eq column_names
      end
    end

    describe 'when data is a StringIO' do
      let(:data) { StringIO.new File.read(dbf_path) }
      let(:table) { DBF::Table.new data, nil, Encoding::US_ASCII }

      it 'is an array of all column names' do
        expect(table.column_names).to eq column_names
      end
    end
  end

  describe '#activerecord_schema_definition' do
    context 'with type N (number)' do
      it 'outputs an integer column' do
        column = DBF::Column.new table, 'ColumnName', 'N', 1, 0
        expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :integer\n"
      end
    end

    describe 'with type B (binary)' do
      context 'with Foxpro dbf' do
        it 'outputs a float column' do
          column = DBF::Column.new table, 'ColumnName', 'B', 1, 2
          expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :binary\n"
        end
      end
    end

    it 'defines a float colmn if type is (N)umber with more than 0 decimals' do
      column = DBF::Column.new table, 'ColumnName', 'N', 1, 2
      expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :float\n"
    end

    it 'defines a date column if type is (D)ate' do
      column = DBF::Column.new table, 'ColumnName', 'D', 8, 0
      expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :date\n"
    end

    it 'defines a datetime column if type is (D)ate' do
      column = DBF::Column.new table, 'ColumnName', 'T', 16, 0
      expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :datetime\n"
    end

    it 'defines a boolean column if type is (L)ogical' do
      column = DBF::Column.new table, 'ColumnName', 'L', 1, 0
      expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :boolean\n"
    end

    it 'defines a text column if type is (M)emo' do
      column = DBF::Column.new table, 'ColumnName', 'M', 1, 0
      expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :text\n"
    end

    it 'defines a string column with length for any other data types' do
      column = DBF::Column.new table, 'ColumnName', 'X', 20, 0
      expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :string, :limit => 20\n"
    end
  end
end
